tree-sitter-analyzer 0.8.0__py3-none-any.whl → 0.8.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.
Potentially problematic release.
This version of tree-sitter-analyzer might be problematic. Click here for more details.
- tree_sitter_analyzer/cli/commands/base_command.py +27 -9
- tree_sitter_analyzer/cli/commands/default_command.py +1 -1
- tree_sitter_analyzer/cli/commands/partial_read_command.py +7 -7
- tree_sitter_analyzer/cli/commands/query_command.py +11 -4
- tree_sitter_analyzer/cli/commands/table_command.py +1 -1
- tree_sitter_analyzer/cli/info_commands.py +2 -2
- tree_sitter_analyzer/cli_main.py +13 -7
- tree_sitter_analyzer/core/analysis_engine.py +38 -13
- tree_sitter_analyzer/mcp/server.py +74 -12
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +22 -3
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +10 -2
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +20 -3
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +18 -2
- tree_sitter_analyzer/project_detector.py +317 -0
- tree_sitter_analyzer/security/validator.py +24 -7
- {tree_sitter_analyzer-0.8.0.dist-info → tree_sitter_analyzer-0.8.1.dist-info}/METADATA +24 -1
- {tree_sitter_analyzer-0.8.0.dist-info → tree_sitter_analyzer-0.8.1.dist-info}/RECORD +19 -18
- {tree_sitter_analyzer-0.8.0.dist-info → tree_sitter_analyzer-0.8.1.dist-info}/WHEEL +0 -0
- {tree_sitter_analyzer-0.8.0.dist-info → tree_sitter_analyzer-0.8.1.dist-info}/entry_points.txt +0 -0
|
@@ -15,6 +15,8 @@ from ...file_handler import read_file_partial
|
|
|
15
15
|
from ...language_detector import detect_language_from_file, is_language_supported
|
|
16
16
|
from ...models import AnalysisResult
|
|
17
17
|
from ...output_manager import output_error, output_info
|
|
18
|
+
from ...project_detector import detect_project_root
|
|
19
|
+
from ...security import SecurityValidator
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
class BaseCommand(ABC):
|
|
@@ -28,18 +30,32 @@ class BaseCommand(ABC):
|
|
|
28
30
|
def __init__(self, args: Namespace):
|
|
29
31
|
"""Initialize command with parsed arguments."""
|
|
30
32
|
self.args = args
|
|
31
|
-
|
|
33
|
+
|
|
34
|
+
# Detect project root with priority handling
|
|
35
|
+
file_path = getattr(args, 'file_path', None)
|
|
36
|
+
explicit_root = getattr(args, 'project_root', None)
|
|
37
|
+
self.project_root = detect_project_root(file_path, explicit_root)
|
|
38
|
+
|
|
39
|
+
# Initialize components with project root
|
|
40
|
+
self.analysis_engine = get_analysis_engine(self.project_root)
|
|
41
|
+
self.security_validator = SecurityValidator(self.project_root)
|
|
32
42
|
|
|
33
43
|
def validate_file(self) -> bool:
|
|
34
44
|
"""Validate input file exists and is accessible."""
|
|
35
45
|
if not hasattr(self.args, "file_path") or not self.args.file_path:
|
|
36
|
-
output_error("
|
|
46
|
+
output_error("File path not specified.")
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
# Security validation
|
|
50
|
+
is_valid, error_msg = self.security_validator.validate_file_path(self.args.file_path)
|
|
51
|
+
if not is_valid:
|
|
52
|
+
output_error(f"Invalid file path: {error_msg}")
|
|
37
53
|
return False
|
|
38
54
|
|
|
39
55
|
import os
|
|
40
56
|
|
|
41
57
|
if not os.path.exists(self.args.file_path):
|
|
42
|
-
output_error(f"
|
|
58
|
+
output_error(f"File not found: {self.args.file_path}")
|
|
43
59
|
return False
|
|
44
60
|
|
|
45
61
|
return True
|
|
@@ -47,7 +63,9 @@ class BaseCommand(ABC):
|
|
|
47
63
|
def detect_language(self) -> str | None:
|
|
48
64
|
"""Detect or validate the target language."""
|
|
49
65
|
if hasattr(self.args, "language") and self.args.language:
|
|
50
|
-
|
|
66
|
+
# Sanitize language input
|
|
67
|
+
sanitized_language = self.security_validator.sanitize_input(self.args.language, max_length=50)
|
|
68
|
+
target_language = sanitized_language.lower()
|
|
51
69
|
if (not hasattr(self.args, "table") or not self.args.table) and (
|
|
52
70
|
not hasattr(self.args, "quiet") or not self.args.quiet
|
|
53
71
|
):
|
|
@@ -94,10 +112,10 @@ class BaseCommand(ABC):
|
|
|
94
112
|
end_column=getattr(self.args, "end_column", None),
|
|
95
113
|
)
|
|
96
114
|
if partial_content is None:
|
|
97
|
-
output_error("
|
|
115
|
+
output_error("Failed to read file partially")
|
|
98
116
|
return None
|
|
99
117
|
except Exception as e:
|
|
100
|
-
output_error(f"
|
|
118
|
+
output_error(f"Failed to read file partially: {e}")
|
|
101
119
|
return None
|
|
102
120
|
|
|
103
121
|
request = AnalysisRequest(
|
|
@@ -114,13 +132,13 @@ class BaseCommand(ABC):
|
|
|
114
132
|
if analysis_result
|
|
115
133
|
else "Unknown error"
|
|
116
134
|
)
|
|
117
|
-
output_error(f"
|
|
135
|
+
output_error(f"Analysis failed: {error_msg}")
|
|
118
136
|
return None
|
|
119
137
|
|
|
120
138
|
return analysis_result
|
|
121
139
|
|
|
122
140
|
except Exception as e:
|
|
123
|
-
output_error(f"
|
|
141
|
+
output_error(f"An error occurred during analysis: {e}")
|
|
124
142
|
return None
|
|
125
143
|
|
|
126
144
|
def execute(self) -> int:
|
|
@@ -143,7 +161,7 @@ class BaseCommand(ABC):
|
|
|
143
161
|
try:
|
|
144
162
|
return asyncio.run(self.execute_async(language))
|
|
145
163
|
except Exception as e:
|
|
146
|
-
output_error(f"
|
|
164
|
+
output_error(f"An error occurred during command execution: {e}")
|
|
147
165
|
return 1
|
|
148
166
|
|
|
149
167
|
@abstractmethod
|
|
@@ -14,5 +14,5 @@ class DefaultCommand(BaseCommand):
|
|
|
14
14
|
|
|
15
15
|
async def execute_async(self, language: str) -> int:
|
|
16
16
|
"""Execute default command - show error for missing options."""
|
|
17
|
-
output_error("
|
|
17
|
+
output_error("Please specify a query or --advanced option")
|
|
18
18
|
return 1
|
|
@@ -28,7 +28,7 @@ class PartialReadCommand(BaseCommand):
|
|
|
28
28
|
if not hasattr(self.args, "file_path") or not self.args.file_path:
|
|
29
29
|
from ...output_manager import output_error
|
|
30
30
|
|
|
31
|
-
output_error("
|
|
31
|
+
output_error("File path not specified.")
|
|
32
32
|
return False
|
|
33
33
|
|
|
34
34
|
import os
|
|
@@ -36,7 +36,7 @@ class PartialReadCommand(BaseCommand):
|
|
|
36
36
|
if not os.path.exists(self.args.file_path):
|
|
37
37
|
from ...output_manager import output_error
|
|
38
38
|
|
|
39
|
-
output_error(f"
|
|
39
|
+
output_error(f"File not found: {self.args.file_path}")
|
|
40
40
|
return False
|
|
41
41
|
|
|
42
42
|
return True
|
|
@@ -56,20 +56,20 @@ class PartialReadCommand(BaseCommand):
|
|
|
56
56
|
if not self.args.start_line:
|
|
57
57
|
from ...output_manager import output_error
|
|
58
58
|
|
|
59
|
-
output_error("
|
|
59
|
+
output_error("--start-line is required")
|
|
60
60
|
return 1
|
|
61
61
|
|
|
62
62
|
if self.args.start_line < 1:
|
|
63
63
|
from ...output_manager import output_error
|
|
64
64
|
|
|
65
|
-
output_error("
|
|
65
|
+
output_error("--start-line must be 1 or greater")
|
|
66
66
|
return 1
|
|
67
67
|
|
|
68
68
|
if self.args.end_line and self.args.end_line < self.args.start_line:
|
|
69
69
|
from ...output_manager import output_error
|
|
70
70
|
|
|
71
71
|
output_error(
|
|
72
|
-
"
|
|
72
|
+
"--end-line must be greater than or equal to --start-line"
|
|
73
73
|
)
|
|
74
74
|
return 1
|
|
75
75
|
|
|
@@ -86,7 +86,7 @@ class PartialReadCommand(BaseCommand):
|
|
|
86
86
|
if partial_content is None:
|
|
87
87
|
from ...output_manager import output_error
|
|
88
88
|
|
|
89
|
-
output_error("
|
|
89
|
+
output_error("Failed to read file partially")
|
|
90
90
|
return 1
|
|
91
91
|
|
|
92
92
|
# Output the result
|
|
@@ -96,7 +96,7 @@ class PartialReadCommand(BaseCommand):
|
|
|
96
96
|
except Exception as e:
|
|
97
97
|
from ...output_manager import output_error
|
|
98
98
|
|
|
99
|
-
output_error(f"
|
|
99
|
+
output_error(f"Failed to read file partially: {e}")
|
|
100
100
|
return 1
|
|
101
101
|
|
|
102
102
|
def _output_partial_content(self, content: str) -> None:
|
|
@@ -18,21 +18,28 @@ class QueryCommand(BaseCommand):
|
|
|
18
18
|
query_to_execute = None
|
|
19
19
|
|
|
20
20
|
if hasattr(self.args, "query_key") and self.args.query_key:
|
|
21
|
+
# Sanitize query key input
|
|
22
|
+
sanitized_query_key = self.security_validator.sanitize_input(self.args.query_key, max_length=100)
|
|
21
23
|
try:
|
|
22
|
-
query_to_execute = query_loader.get_query(language,
|
|
24
|
+
query_to_execute = query_loader.get_query(language, sanitized_query_key)
|
|
23
25
|
if query_to_execute is None:
|
|
24
26
|
output_error(
|
|
25
|
-
f"ERROR: Query '{
|
|
27
|
+
f"ERROR: Query '{sanitized_query_key}' not found for language '{language}'"
|
|
26
28
|
)
|
|
27
29
|
return 1
|
|
28
30
|
except ValueError as e:
|
|
29
|
-
output_error(f"
|
|
31
|
+
output_error(f"{e}")
|
|
30
32
|
return 1
|
|
31
33
|
elif hasattr(self.args, "query_string") and self.args.query_string:
|
|
34
|
+
# Security check for query string (potential regex patterns)
|
|
35
|
+
is_safe, error_msg = self.security_validator.regex_checker.validate_pattern(self.args.query_string)
|
|
36
|
+
if not is_safe:
|
|
37
|
+
output_error(f"Unsafe query pattern: {error_msg}")
|
|
38
|
+
return 1
|
|
32
39
|
query_to_execute = self.args.query_string
|
|
33
40
|
|
|
34
41
|
if not query_to_execute:
|
|
35
|
-
output_error("
|
|
42
|
+
output_error("No query specified.")
|
|
36
43
|
return 1
|
|
37
44
|
|
|
38
45
|
# Perform analysis
|
|
@@ -42,7 +42,7 @@ class TableCommand(BaseCommand):
|
|
|
42
42
|
return 0
|
|
43
43
|
|
|
44
44
|
except Exception as e:
|
|
45
|
-
output_error(f"
|
|
45
|
+
output_error(f"An error occurred during table format analysis: {e}")
|
|
46
46
|
return 1
|
|
47
47
|
|
|
48
48
|
def _convert_to_structure_format(
|
|
@@ -78,7 +78,7 @@ class DescribeQueryCommand(InfoCommand):
|
|
|
78
78
|
|
|
79
79
|
if query_description is None or query_content is None:
|
|
80
80
|
output_error(
|
|
81
|
-
f"
|
|
81
|
+
f"Query '{self.args.describe_query}' not found for language '{language}'"
|
|
82
82
|
)
|
|
83
83
|
return 1
|
|
84
84
|
|
|
@@ -87,7 +87,7 @@ class DescribeQueryCommand(InfoCommand):
|
|
|
87
87
|
)
|
|
88
88
|
output_data(f"Query content:\n{query_content}")
|
|
89
89
|
except ValueError as e:
|
|
90
|
-
output_error(f"
|
|
90
|
+
output_error(f"{e}")
|
|
91
91
|
return 1
|
|
92
92
|
return 0
|
|
93
93
|
|
tree_sitter_analyzer/cli_main.py
CHANGED
|
@@ -166,6 +166,12 @@ def create_argument_parser() -> argparse.ArgumentParser:
|
|
|
166
166
|
help="Explicitly specify language (auto-detected from extension if omitted)",
|
|
167
167
|
)
|
|
168
168
|
|
|
169
|
+
# Project options
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--project-root",
|
|
172
|
+
help="Project root directory for security validation (auto-detected if not specified)",
|
|
173
|
+
)
|
|
174
|
+
|
|
169
175
|
# Logging options
|
|
170
176
|
parser.add_argument(
|
|
171
177
|
"--quiet",
|
|
@@ -201,25 +207,25 @@ def handle_special_commands(args: argparse.Namespace) -> int | None:
|
|
|
201
207
|
# Validate partial read options
|
|
202
208
|
if hasattr(args, "partial_read") and args.partial_read:
|
|
203
209
|
if args.start_line is None:
|
|
204
|
-
output_error("
|
|
210
|
+
output_error("--start-line is required")
|
|
205
211
|
return 1
|
|
206
212
|
|
|
207
213
|
if args.start_line < 1:
|
|
208
|
-
output_error("
|
|
214
|
+
output_error("--start-line must be 1 or greater")
|
|
209
215
|
return 1
|
|
210
216
|
|
|
211
217
|
if args.end_line and args.end_line < args.start_line:
|
|
212
218
|
output_error(
|
|
213
|
-
"
|
|
219
|
+
"--end-line must be greater than or equal to --start-line"
|
|
214
220
|
)
|
|
215
221
|
return 1
|
|
216
222
|
|
|
217
223
|
if args.start_column is not None and args.start_column < 0:
|
|
218
|
-
output_error("
|
|
224
|
+
output_error("--start-column must be 0 or greater")
|
|
219
225
|
return 1
|
|
220
226
|
|
|
221
227
|
if args.end_column is not None and args.end_column < 0:
|
|
222
|
-
output_error("
|
|
228
|
+
output_error("--end-column must be 0 or greater")
|
|
223
229
|
return 1
|
|
224
230
|
|
|
225
231
|
# Query language commands
|
|
@@ -279,9 +285,9 @@ def main() -> None:
|
|
|
279
285
|
sys.exit(exit_code)
|
|
280
286
|
else:
|
|
281
287
|
if not args.file_path:
|
|
282
|
-
output_error("
|
|
288
|
+
output_error("File path not specified.")
|
|
283
289
|
else:
|
|
284
|
-
output_error("
|
|
290
|
+
output_error("No executable command specified.")
|
|
285
291
|
parser.print_help()
|
|
286
292
|
sys.exit(1)
|
|
287
293
|
|
|
@@ -15,11 +15,12 @@ Roo Code compliance:
|
|
|
15
15
|
import hashlib
|
|
16
16
|
import threading
|
|
17
17
|
from dataclasses import dataclass
|
|
18
|
-
from typing import Any, Optional, Protocol
|
|
18
|
+
from typing import Any, Dict, Optional, Protocol
|
|
19
19
|
|
|
20
20
|
from ..models import AnalysisResult
|
|
21
21
|
from ..plugins.base import LanguagePlugin as BaseLanguagePlugin
|
|
22
22
|
from ..plugins.manager import PluginManager
|
|
23
|
+
from ..security import SecurityValidator
|
|
23
24
|
from ..utils import log_debug, log_error, log_info, log_performance
|
|
24
25
|
from .cache_service import CacheService
|
|
25
26
|
|
|
@@ -204,32 +205,41 @@ class UnifiedAnalysisEngine:
|
|
|
204
205
|
_performance_monitor: パフォーマンス監視
|
|
205
206
|
"""
|
|
206
207
|
|
|
207
|
-
|
|
208
|
+
_instances: Dict[str, "UnifiedAnalysisEngine"] = {}
|
|
208
209
|
_lock: threading.Lock = threading.Lock()
|
|
209
210
|
|
|
210
|
-
def __new__(cls) -> "UnifiedAnalysisEngine":
|
|
211
|
-
"""シングルトンパターンでインスタンス共有"""
|
|
212
|
-
|
|
211
|
+
def __new__(cls, project_root: str = None) -> "UnifiedAnalysisEngine":
|
|
212
|
+
"""シングルトンパターンでインスタンス共有 (project_root aware)"""
|
|
213
|
+
# Create a key based on project_root for different instances
|
|
214
|
+
instance_key = project_root or "default"
|
|
215
|
+
|
|
216
|
+
if instance_key not in cls._instances:
|
|
213
217
|
with cls._lock:
|
|
214
|
-
if cls.
|
|
215
|
-
|
|
216
|
-
|
|
218
|
+
if instance_key not in cls._instances:
|
|
219
|
+
instance = super().__new__(cls)
|
|
220
|
+
cls._instances[instance_key] = instance
|
|
221
|
+
# Mark as not initialized for this instance
|
|
222
|
+
instance._initialized = False
|
|
217
223
|
|
|
218
|
-
|
|
224
|
+
return cls._instances[instance_key]
|
|
225
|
+
|
|
226
|
+
def __init__(self, project_root: str = None) -> None:
|
|
219
227
|
"""初期化(一度のみ実行)"""
|
|
220
|
-
if hasattr(self, "_initialized"):
|
|
228
|
+
if hasattr(self, "_initialized") and self._initialized:
|
|
221
229
|
return
|
|
222
230
|
|
|
223
231
|
self._cache_service = CacheService()
|
|
224
232
|
self._plugin_manager = PluginManager()
|
|
225
233
|
self._performance_monitor = PerformanceMonitor()
|
|
234
|
+
self._security_validator = SecurityValidator(project_root)
|
|
235
|
+
self._project_root = project_root
|
|
226
236
|
|
|
227
237
|
# プラグインを自動ロード
|
|
228
238
|
self._load_plugins()
|
|
229
239
|
|
|
230
240
|
self._initialized = True
|
|
231
241
|
|
|
232
|
-
log_info("UnifiedAnalysisEngine initialized")
|
|
242
|
+
log_info(f"UnifiedAnalysisEngine initialized with project root: {project_root}")
|
|
233
243
|
|
|
234
244
|
def _load_plugins(self) -> None:
|
|
235
245
|
"""利用可能なプラグインを自動ロード"""
|
|
@@ -265,6 +275,12 @@ class UnifiedAnalysisEngine:
|
|
|
265
275
|
"""
|
|
266
276
|
log_info(f"Starting analysis for {request.file_path}")
|
|
267
277
|
|
|
278
|
+
# Security validation
|
|
279
|
+
is_valid, error_msg = self._security_validator.validate_file_path(request.file_path)
|
|
280
|
+
if not is_valid:
|
|
281
|
+
log_error(f"Security validation failed for file path: {request.file_path} - {error_msg}")
|
|
282
|
+
raise ValueError(f"Invalid file path: {error_msg}")
|
|
283
|
+
|
|
268
284
|
# キャッシュチェック(CLI・MCP間で共有)
|
|
269
285
|
cache_key = self._generate_cache_key(request)
|
|
270
286
|
cached_result = await self._cache_service.get(cache_key)
|
|
@@ -323,6 +339,12 @@ class UnifiedAnalysisEngine:
|
|
|
323
339
|
Returns:
|
|
324
340
|
Analysis result
|
|
325
341
|
"""
|
|
342
|
+
# Security validation
|
|
343
|
+
is_valid, error_msg = self._security_validator.validate_file_path(file_path)
|
|
344
|
+
if not is_valid:
|
|
345
|
+
log_error(f"Security validation failed for file path: {file_path} - {error_msg}")
|
|
346
|
+
raise ValueError(f"Invalid file path: {error_msg}")
|
|
347
|
+
|
|
326
348
|
request = AnalysisRequest(
|
|
327
349
|
file_path=file_path,
|
|
328
350
|
language=None, # Auto-detect
|
|
@@ -545,11 +567,14 @@ class MockLanguagePlugin:
|
|
|
545
567
|
)
|
|
546
568
|
|
|
547
569
|
|
|
548
|
-
def get_analysis_engine() -> UnifiedAnalysisEngine:
|
|
570
|
+
def get_analysis_engine(project_root: str = None) -> UnifiedAnalysisEngine:
|
|
549
571
|
"""
|
|
550
572
|
統一解析エンジンのインスタンスを取得
|
|
551
573
|
|
|
574
|
+
Args:
|
|
575
|
+
project_root: Project root directory for security validation
|
|
576
|
+
|
|
552
577
|
Returns:
|
|
553
578
|
統一解析エンジンのシングルトンインスタンス
|
|
554
579
|
"""
|
|
555
|
-
return UnifiedAnalysisEngine()
|
|
580
|
+
return UnifiedAnalysisEngine(project_root)
|
|
@@ -6,8 +6,10 @@ This module provides the main MCP server that exposes tree-sitter analyzer
|
|
|
6
6
|
functionality through the Model Context Protocol.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import argparse
|
|
9
10
|
import asyncio
|
|
10
11
|
import json
|
|
12
|
+
import os
|
|
11
13
|
import sys
|
|
12
14
|
from typing import Any
|
|
13
15
|
|
|
@@ -43,6 +45,8 @@ except ImportError:
|
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
from ..core.analysis_engine import get_analysis_engine
|
|
48
|
+
from ..project_detector import detect_project_root
|
|
49
|
+
from ..security import SecurityValidator
|
|
46
50
|
from ..utils import setup_logger
|
|
47
51
|
from . import MCP_INFO
|
|
48
52
|
from .resources import CodeFileResource, ProjectStatsResource
|
|
@@ -64,16 +68,17 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
64
68
|
integrating with existing analyzer components.
|
|
65
69
|
"""
|
|
66
70
|
|
|
67
|
-
def __init__(self) -> None:
|
|
71
|
+
def __init__(self, project_root: str = None) -> None:
|
|
68
72
|
"""Initialize the MCP server with analyzer components."""
|
|
69
73
|
self.server: Server | None = None
|
|
70
|
-
self.analysis_engine = get_analysis_engine()
|
|
74
|
+
self.analysis_engine = get_analysis_engine(project_root)
|
|
75
|
+
self.security_validator = SecurityValidator(project_root)
|
|
71
76
|
# Use unified analysis engine instead of deprecated AdvancedAnalyzer
|
|
72
77
|
|
|
73
|
-
# Initialize MCP tools
|
|
74
|
-
self.read_partial_tool: MCPTool = ReadPartialTool()
|
|
75
|
-
self.universal_analyze_tool: MCPTool = UniversalAnalyzeTool()
|
|
76
|
-
self.table_format_tool: MCPTool = TableFormatTool()
|
|
78
|
+
# Initialize MCP tools with security validation
|
|
79
|
+
self.read_partial_tool: MCPTool = ReadPartialTool(project_root)
|
|
80
|
+
self.universal_analyze_tool: MCPTool = UniversalAnalyzeTool(project_root)
|
|
81
|
+
self.table_format_tool: MCPTool = TableFormatTool(project_root)
|
|
77
82
|
|
|
78
83
|
# Initialize MCP resources
|
|
79
84
|
self.code_file_resource = CodeFileResource()
|
|
@@ -161,9 +166,25 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
161
166
|
async def handle_call_tool(
|
|
162
167
|
name: str, arguments: dict[str, Any]
|
|
163
168
|
) -> list[TextContent]:
|
|
164
|
-
"""Handle tool calls."""
|
|
169
|
+
"""Handle tool calls with security validation."""
|
|
165
170
|
try:
|
|
166
|
-
|
|
171
|
+
# Security validation for tool name
|
|
172
|
+
sanitized_name = self.security_validator.sanitize_input(name, max_length=100)
|
|
173
|
+
|
|
174
|
+
# Log tool call for audit
|
|
175
|
+
logger.info(f"MCP tool call: {sanitized_name} with args: {list(arguments.keys())}")
|
|
176
|
+
|
|
177
|
+
# Validate arguments contain no malicious content
|
|
178
|
+
for key, value in arguments.items():
|
|
179
|
+
if isinstance(value, str):
|
|
180
|
+
# Check for potential injection attempts
|
|
181
|
+
if len(value) > 10000: # Prevent extremely large inputs
|
|
182
|
+
raise ValueError(f"Input too large for parameter {key}")
|
|
183
|
+
|
|
184
|
+
# Basic sanitization for string inputs
|
|
185
|
+
sanitized_value = self.security_validator.sanitize_input(value, max_length=10000)
|
|
186
|
+
arguments[key] = sanitized_value
|
|
187
|
+
if sanitized_name == "analyze_code_scale":
|
|
167
188
|
result = await self._analyze_code_scale(arguments)
|
|
168
189
|
return [
|
|
169
190
|
TextContent(
|
|
@@ -171,7 +192,7 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
171
192
|
text=json.dumps(result, indent=2, ensure_ascii=False),
|
|
172
193
|
)
|
|
173
194
|
]
|
|
174
|
-
elif
|
|
195
|
+
elif sanitized_name == "read_code_partial":
|
|
175
196
|
result = await self.read_partial_tool.execute(arguments)
|
|
176
197
|
return [
|
|
177
198
|
TextContent(
|
|
@@ -179,7 +200,7 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
179
200
|
text=json.dumps(result, indent=2, ensure_ascii=False),
|
|
180
201
|
)
|
|
181
202
|
]
|
|
182
|
-
elif
|
|
203
|
+
elif sanitized_name == "format_table":
|
|
183
204
|
result = await self.table_format_tool.execute(arguments)
|
|
184
205
|
return [
|
|
185
206
|
TextContent(
|
|
@@ -187,7 +208,7 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
187
208
|
text=json.dumps(result, indent=2, ensure_ascii=False),
|
|
188
209
|
)
|
|
189
210
|
]
|
|
190
|
-
elif
|
|
211
|
+
elif sanitized_name == "analyze_code_universal":
|
|
191
212
|
result = await self.universal_analyze_tool.execute(arguments)
|
|
192
213
|
return [
|
|
193
214
|
TextContent(
|
|
@@ -318,10 +339,51 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
318
339
|
pass # Silently ignore logging errors during shutdown
|
|
319
340
|
|
|
320
341
|
|
|
342
|
+
def parse_mcp_args(args=None) -> argparse.Namespace:
|
|
343
|
+
"""Parse command line arguments for MCP server."""
|
|
344
|
+
parser = argparse.ArgumentParser(
|
|
345
|
+
description="Tree-sitter Analyzer MCP Server",
|
|
346
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
347
|
+
epilog="""
|
|
348
|
+
Environment Variables:
|
|
349
|
+
TREE_SITTER_PROJECT_ROOT Project root directory (alternative to --project-root)
|
|
350
|
+
|
|
351
|
+
Examples:
|
|
352
|
+
python -m tree_sitter_analyzer.mcp.server
|
|
353
|
+
python -m tree_sitter_analyzer.mcp.server --project-root /path/to/project
|
|
354
|
+
"""
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
parser.add_argument(
|
|
358
|
+
"--project-root",
|
|
359
|
+
help="Project root directory for security validation (auto-detected if not specified)"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
return parser.parse_args(args)
|
|
363
|
+
|
|
364
|
+
|
|
321
365
|
async def main() -> None:
|
|
322
366
|
"""Main entry point for the MCP server."""
|
|
323
367
|
try:
|
|
324
|
-
|
|
368
|
+
# Parse command line arguments (empty list for testing)
|
|
369
|
+
args = parse_mcp_args([])
|
|
370
|
+
|
|
371
|
+
# Determine project root with priority handling
|
|
372
|
+
project_root = None
|
|
373
|
+
|
|
374
|
+
# Priority 1: Command line argument
|
|
375
|
+
if args.project_root:
|
|
376
|
+
project_root = args.project_root
|
|
377
|
+
# Priority 2: Environment variable
|
|
378
|
+
elif os.getenv('TREE_SITTER_PROJECT_ROOT'):
|
|
379
|
+
project_root = os.getenv('TREE_SITTER_PROJECT_ROOT')
|
|
380
|
+
# Priority 3: Auto-detection from current directory
|
|
381
|
+
else:
|
|
382
|
+
project_root = detect_project_root()
|
|
383
|
+
|
|
384
|
+
logger.info(f"MCP server starting with project root: {project_root}")
|
|
385
|
+
|
|
386
|
+
server = TreeSitterAnalyzerMCPServer(project_root)
|
|
325
387
|
await server.run()
|
|
326
388
|
except KeyboardInterrupt:
|
|
327
389
|
try:
|
|
@@ -13,6 +13,7 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
from ...core.analysis_engine import AnalysisRequest, get_analysis_engine
|
|
15
15
|
from ...language_detector import detect_language_from_file
|
|
16
|
+
from ...security import SecurityValidator
|
|
16
17
|
from ...utils import setup_logger
|
|
17
18
|
|
|
18
19
|
# Set up logging
|
|
@@ -28,11 +29,13 @@ class AnalyzeScaleTool:
|
|
|
28
29
|
for LLM workflow efficiency.
|
|
29
30
|
"""
|
|
30
31
|
|
|
31
|
-
def __init__(self) -> None:
|
|
32
|
+
def __init__(self, project_root: str = None) -> None:
|
|
32
33
|
"""Initialize the analyze scale tool."""
|
|
33
34
|
# Use unified analysis engine instead of deprecated AdvancedAnalyzer
|
|
34
|
-
self.
|
|
35
|
-
|
|
35
|
+
self.project_root = project_root
|
|
36
|
+
self.analysis_engine = get_analysis_engine(project_root)
|
|
37
|
+
self.security_validator = SecurityValidator(project_root)
|
|
38
|
+
logger.info("AnalyzeScaleTool initialized with security validation")
|
|
36
39
|
|
|
37
40
|
def _calculate_file_metrics(self, file_path: str) -> dict[str, Any]:
|
|
38
41
|
"""
|
|
@@ -251,6 +254,12 @@ class AnalyzeScaleTool:
|
|
|
251
254
|
if total_lines > 200:
|
|
252
255
|
guidance["recommended_tools"].append("read_code_partial")
|
|
253
256
|
|
|
257
|
+
# Ensure all required fields exist
|
|
258
|
+
required_fields = ["complexity_hotspots", "classes", "methods", "fields", "imports"]
|
|
259
|
+
for field in required_fields:
|
|
260
|
+
if field not in structural_overview:
|
|
261
|
+
structural_overview[field] = []
|
|
262
|
+
|
|
254
263
|
if len(structural_overview["complexity_hotspots"]) > 0:
|
|
255
264
|
guidance["recommended_tools"].append("format_table")
|
|
256
265
|
guidance["complexity_assessment"] = (
|
|
@@ -339,6 +348,16 @@ class AnalyzeScaleTool:
|
|
|
339
348
|
include_details = arguments.get("include_details", False)
|
|
340
349
|
include_guidance = arguments.get("include_guidance", True)
|
|
341
350
|
|
|
351
|
+
# Security validation
|
|
352
|
+
is_valid, error_msg = self.security_validator.validate_file_path(file_path)
|
|
353
|
+
if not is_valid:
|
|
354
|
+
logger.warning(f"Security validation failed for file path: {file_path} - {error_msg}")
|
|
355
|
+
raise ValueError(f"Invalid file path: {error_msg}")
|
|
356
|
+
|
|
357
|
+
# Sanitize inputs
|
|
358
|
+
if language:
|
|
359
|
+
language = self.security_validator.sanitize_input(language, max_length=50)
|
|
360
|
+
|
|
342
361
|
# Validate file exists
|
|
343
362
|
if not Path(file_path).exists():
|
|
344
363
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
from ...file_handler import read_file_partial
|
|
14
|
+
from ...security import SecurityValidator
|
|
14
15
|
from ...utils import setup_logger
|
|
15
16
|
|
|
16
17
|
# Set up logging
|
|
@@ -25,9 +26,10 @@ class ReadPartialTool:
|
|
|
25
26
|
selective file content reading through the MCP protocol.
|
|
26
27
|
"""
|
|
27
28
|
|
|
28
|
-
def __init__(self) -> None:
|
|
29
|
+
def __init__(self, project_root: str = None) -> None:
|
|
29
30
|
"""Initialize the read partial tool."""
|
|
30
|
-
|
|
31
|
+
self.security_validator = SecurityValidator(project_root)
|
|
32
|
+
logger.info("ReadPartialTool initialized with security validation")
|
|
31
33
|
|
|
32
34
|
def get_tool_schema(self) -> dict[str, Any]:
|
|
33
35
|
"""
|
|
@@ -102,6 +104,12 @@ class ReadPartialTool:
|
|
|
102
104
|
end_column = arguments.get("end_column")
|
|
103
105
|
# output_format = arguments.get("format", "text") # Not used currently
|
|
104
106
|
|
|
107
|
+
# Security validation
|
|
108
|
+
is_valid, error_msg = self.security_validator.validate_file_path(file_path)
|
|
109
|
+
if not is_valid:
|
|
110
|
+
logger.warning(f"Security validation failed for file path: {file_path} - {error_msg}")
|
|
111
|
+
raise ValueError(f"Invalid file path: {error_msg}")
|
|
112
|
+
|
|
105
113
|
# Validate file exists
|
|
106
114
|
if not Path(file_path).exists():
|
|
107
115
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
@@ -11,6 +11,7 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
from ...core.analysis_engine import AnalysisRequest, get_analysis_engine
|
|
13
13
|
from ...language_detector import detect_language_from_file
|
|
14
|
+
from ...security import SecurityValidator
|
|
14
15
|
from ...table_formatter import TableFormatter
|
|
15
16
|
from ...utils import setup_logger
|
|
16
17
|
from ..utils import get_performance_monitor
|
|
@@ -28,11 +29,13 @@ class TableFormatTool:
|
|
|
28
29
|
the CLI --table=full option.
|
|
29
30
|
"""
|
|
30
31
|
|
|
31
|
-
def __init__(self) -> None:
|
|
32
|
+
def __init__(self, project_root: str = None) -> None:
|
|
32
33
|
"""Initialize the table format tool."""
|
|
33
34
|
self.logger = logger
|
|
34
|
-
self.
|
|
35
|
-
|
|
35
|
+
self.project_root = project_root
|
|
36
|
+
self.analysis_engine = get_analysis_engine(project_root)
|
|
37
|
+
self.security_validator = SecurityValidator(project_root)
|
|
38
|
+
logger.info("TableFormatTool initialized with security validation")
|
|
36
39
|
|
|
37
40
|
def get_tool_schema(self) -> dict[str, Any]:
|
|
38
41
|
"""
|
|
@@ -268,6 +271,20 @@ class TableFormatTool:
|
|
|
268
271
|
format_type = args.get("format_type", "full")
|
|
269
272
|
language = args.get("language")
|
|
270
273
|
|
|
274
|
+
# Security validation
|
|
275
|
+
is_valid, error_msg = self.security_validator.validate_file_path(file_path)
|
|
276
|
+
if not is_valid:
|
|
277
|
+
self.logger.warning(f"Security validation failed for file path: {file_path} - {error_msg}")
|
|
278
|
+
raise ValueError(f"Invalid file path: {error_msg}")
|
|
279
|
+
|
|
280
|
+
# Sanitize format_type input
|
|
281
|
+
if format_type:
|
|
282
|
+
format_type = self.security_validator.sanitize_input(format_type, max_length=50)
|
|
283
|
+
|
|
284
|
+
# Sanitize language input
|
|
285
|
+
if language:
|
|
286
|
+
language = self.security_validator.sanitize_input(language, max_length=50)
|
|
287
|
+
|
|
271
288
|
# Validate file exists
|
|
272
289
|
if not Path(file_path).exists():
|
|
273
290
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
@@ -12,6 +12,7 @@ from typing import Any
|
|
|
12
12
|
|
|
13
13
|
from ...core.analysis_engine import AnalysisRequest, get_analysis_engine
|
|
14
14
|
from ...language_detector import detect_language_from_file, is_language_supported
|
|
15
|
+
from ...security import SecurityValidator
|
|
15
16
|
from ..utils import get_performance_monitor
|
|
16
17
|
from ..utils.error_handler import handle_mcp_errors
|
|
17
18
|
|
|
@@ -26,10 +27,13 @@ class UniversalAnalyzeTool:
|
|
|
26
27
|
the appropriate analyzer to provide comprehensive code analysis.
|
|
27
28
|
"""
|
|
28
29
|
|
|
29
|
-
def __init__(self) -> None:
|
|
30
|
+
def __init__(self, project_root: str = None) -> None:
|
|
30
31
|
"""Initialize the universal analysis tool"""
|
|
31
32
|
# Use unified analysis engine instead of deprecated AdvancedAnalyzer
|
|
32
|
-
self.
|
|
33
|
+
self.project_root = project_root
|
|
34
|
+
self.analysis_engine = get_analysis_engine(project_root)
|
|
35
|
+
self.security_validator = SecurityValidator(project_root)
|
|
36
|
+
logger.info("UniversalAnalyzeTool initialized with security validation")
|
|
33
37
|
|
|
34
38
|
def get_tool_definition(self) -> dict[str, Any]:
|
|
35
39
|
"""
|
|
@@ -96,6 +100,18 @@ class UniversalAnalyzeTool:
|
|
|
96
100
|
file_path = arguments["file_path"]
|
|
97
101
|
language = arguments.get("language")
|
|
98
102
|
analysis_type = arguments.get("analysis_type", "basic")
|
|
103
|
+
|
|
104
|
+
# Security validation
|
|
105
|
+
is_valid, error_msg = self.security_validator.validate_file_path(file_path)
|
|
106
|
+
if not is_valid:
|
|
107
|
+
logger.warning(f"Security validation failed for file path: {file_path} - {error_msg}")
|
|
108
|
+
raise ValueError(f"Invalid file path: {error_msg}")
|
|
109
|
+
|
|
110
|
+
# Sanitize inputs
|
|
111
|
+
if language:
|
|
112
|
+
language = self.security_validator.sanitize_input(language, max_length=50)
|
|
113
|
+
if analysis_type:
|
|
114
|
+
analysis_type = self.security_validator.sanitize_input(analysis_type, max_length=50)
|
|
99
115
|
include_ast = arguments.get("include_ast", False)
|
|
100
116
|
include_queries = arguments.get("include_queries", False)
|
|
101
117
|
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Project Root Detection
|
|
4
|
+
|
|
5
|
+
Intelligent detection of project root directories based on common project markers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, List, Tuple
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Common project root indicators (in priority order)
|
|
16
|
+
PROJECT_MARKERS = [
|
|
17
|
+
# Version control
|
|
18
|
+
'.git',
|
|
19
|
+
'.hg',
|
|
20
|
+
'.svn',
|
|
21
|
+
|
|
22
|
+
# Python projects
|
|
23
|
+
'pyproject.toml',
|
|
24
|
+
'setup.py',
|
|
25
|
+
'setup.cfg',
|
|
26
|
+
'requirements.txt',
|
|
27
|
+
'Pipfile',
|
|
28
|
+
'poetry.lock',
|
|
29
|
+
'conda.yaml',
|
|
30
|
+
'environment.yml',
|
|
31
|
+
|
|
32
|
+
# JavaScript/Node.js projects
|
|
33
|
+
'package.json',
|
|
34
|
+
'package-lock.json',
|
|
35
|
+
'yarn.lock',
|
|
36
|
+
'node_modules',
|
|
37
|
+
|
|
38
|
+
# Java projects
|
|
39
|
+
'pom.xml',
|
|
40
|
+
'build.gradle',
|
|
41
|
+
'build.gradle.kts',
|
|
42
|
+
'gradlew',
|
|
43
|
+
'mvnw',
|
|
44
|
+
|
|
45
|
+
# C/C++ projects
|
|
46
|
+
'CMakeLists.txt',
|
|
47
|
+
'Makefile',
|
|
48
|
+
'configure.ac',
|
|
49
|
+
'configure.in',
|
|
50
|
+
|
|
51
|
+
# Rust projects
|
|
52
|
+
'Cargo.toml',
|
|
53
|
+
'Cargo.lock',
|
|
54
|
+
|
|
55
|
+
# Go projects
|
|
56
|
+
'go.mod',
|
|
57
|
+
'go.sum',
|
|
58
|
+
|
|
59
|
+
# .NET projects
|
|
60
|
+
'*.sln',
|
|
61
|
+
'*.csproj',
|
|
62
|
+
'*.vbproj',
|
|
63
|
+
'*.fsproj',
|
|
64
|
+
|
|
65
|
+
# Other common markers
|
|
66
|
+
'README.md',
|
|
67
|
+
'README.rst',
|
|
68
|
+
'README.txt',
|
|
69
|
+
'LICENSE',
|
|
70
|
+
'CHANGELOG.md',
|
|
71
|
+
'.gitignore',
|
|
72
|
+
'.dockerignore',
|
|
73
|
+
'Dockerfile',
|
|
74
|
+
'docker-compose.yml',
|
|
75
|
+
'.editorconfig',
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ProjectRootDetector:
|
|
80
|
+
"""Intelligent project root directory detection."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, max_depth: int = 10):
|
|
83
|
+
"""
|
|
84
|
+
Initialize project root detector.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
max_depth: Maximum directory levels to traverse upward
|
|
88
|
+
"""
|
|
89
|
+
self.max_depth = max_depth
|
|
90
|
+
|
|
91
|
+
def detect_from_file(self, file_path: str) -> Optional[str]:
|
|
92
|
+
"""
|
|
93
|
+
Detect project root from a file path.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
file_path: Path to a file within the project
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Project root directory path, or None if not detected
|
|
100
|
+
"""
|
|
101
|
+
if not file_path:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# Convert to absolute path and get directory
|
|
106
|
+
abs_path = os.path.abspath(file_path)
|
|
107
|
+
if os.path.isfile(abs_path):
|
|
108
|
+
start_dir = os.path.dirname(abs_path)
|
|
109
|
+
else:
|
|
110
|
+
start_dir = abs_path
|
|
111
|
+
|
|
112
|
+
return self._traverse_upward(start_dir)
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.warning(f"Error detecting project root from {file_path}: {e}")
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def detect_from_cwd(self) -> Optional[str]:
|
|
119
|
+
"""
|
|
120
|
+
Detect project root from current working directory.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Project root directory path, or None if not detected
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
return self._traverse_upward(os.getcwd())
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.warning(f"Error detecting project root from cwd: {e}")
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def _traverse_upward(self, start_dir: str) -> Optional[str]:
|
|
132
|
+
"""
|
|
133
|
+
Traverse upward from start directory looking for project markers.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
start_dir: Directory to start traversal from
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Project root directory path, or None if not found
|
|
140
|
+
"""
|
|
141
|
+
current_dir = os.path.abspath(start_dir)
|
|
142
|
+
candidates = []
|
|
143
|
+
|
|
144
|
+
for depth in range(self.max_depth):
|
|
145
|
+
# Check for project markers in current directory
|
|
146
|
+
markers_found = self._find_markers_in_dir(current_dir)
|
|
147
|
+
|
|
148
|
+
if markers_found:
|
|
149
|
+
# Calculate score based on marker priority and count
|
|
150
|
+
score = self._calculate_score(markers_found)
|
|
151
|
+
candidates.append((current_dir, score, markers_found))
|
|
152
|
+
|
|
153
|
+
# If we find high-priority markers, we can stop early
|
|
154
|
+
if any(marker in ['.git', 'pyproject.toml', 'package.json', 'pom.xml', 'Cargo.toml', 'go.mod']
|
|
155
|
+
for marker in markers_found):
|
|
156
|
+
logger.debug(f"Found high-priority project root: {current_dir} (markers: {markers_found})")
|
|
157
|
+
return current_dir
|
|
158
|
+
|
|
159
|
+
# Move up one directory
|
|
160
|
+
parent_dir = os.path.dirname(current_dir)
|
|
161
|
+
if parent_dir == current_dir: # Reached filesystem root
|
|
162
|
+
break
|
|
163
|
+
current_dir = parent_dir
|
|
164
|
+
|
|
165
|
+
# Return the best candidate if any found
|
|
166
|
+
if candidates:
|
|
167
|
+
# Sort by score (descending) and return the best
|
|
168
|
+
candidates.sort(key=lambda x: x[1], reverse=True)
|
|
169
|
+
best_candidate = candidates[0]
|
|
170
|
+
logger.debug(f"Selected project root: {best_candidate[0]} (score: {best_candidate[1]}, markers: {best_candidate[2]})")
|
|
171
|
+
return best_candidate[0]
|
|
172
|
+
|
|
173
|
+
logger.debug(f"No project root detected from {start_dir}")
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
def _find_markers_in_dir(self, directory: str) -> List[str]:
|
|
177
|
+
"""
|
|
178
|
+
Find project markers in a directory.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
directory: Directory to search in
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
List of found marker names
|
|
185
|
+
"""
|
|
186
|
+
found_markers = []
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
dir_contents = os.listdir(directory)
|
|
190
|
+
|
|
191
|
+
for marker in PROJECT_MARKERS:
|
|
192
|
+
if '*' in marker:
|
|
193
|
+
# Handle glob patterns
|
|
194
|
+
import glob
|
|
195
|
+
pattern = os.path.join(directory, marker)
|
|
196
|
+
if glob.glob(pattern):
|
|
197
|
+
found_markers.append(marker)
|
|
198
|
+
else:
|
|
199
|
+
# Handle exact matches
|
|
200
|
+
if marker in dir_contents:
|
|
201
|
+
found_markers.append(marker)
|
|
202
|
+
|
|
203
|
+
except (OSError, PermissionError) as e:
|
|
204
|
+
logger.debug(f"Cannot access directory {directory}: {e}")
|
|
205
|
+
|
|
206
|
+
return found_markers
|
|
207
|
+
|
|
208
|
+
def _calculate_score(self, markers: List[str]) -> int:
|
|
209
|
+
"""
|
|
210
|
+
Calculate a score for project root candidates based on markers found.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
markers: List of found markers
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Score (higher is better)
|
|
217
|
+
"""
|
|
218
|
+
score = 0
|
|
219
|
+
|
|
220
|
+
# High-priority markers
|
|
221
|
+
high_priority = ['.git', 'pyproject.toml', 'package.json', 'pom.xml', 'Cargo.toml', 'go.mod']
|
|
222
|
+
medium_priority = ['setup.py', 'requirements.txt', 'CMakeLists.txt', 'Makefile']
|
|
223
|
+
|
|
224
|
+
for marker in markers:
|
|
225
|
+
if marker in high_priority:
|
|
226
|
+
score += 100
|
|
227
|
+
elif marker in medium_priority:
|
|
228
|
+
score += 50
|
|
229
|
+
else:
|
|
230
|
+
score += 10
|
|
231
|
+
|
|
232
|
+
# Bonus for multiple markers
|
|
233
|
+
if len(markers) > 1:
|
|
234
|
+
score += len(markers) * 5
|
|
235
|
+
|
|
236
|
+
return score
|
|
237
|
+
|
|
238
|
+
def get_fallback_root(self, file_path: str) -> str:
|
|
239
|
+
"""
|
|
240
|
+
Get fallback project root when detection fails.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
file_path: Original file path
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Fallback directory (file's directory or cwd)
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
if file_path and os.path.exists(file_path):
|
|
250
|
+
if os.path.isfile(file_path):
|
|
251
|
+
return os.path.dirname(os.path.abspath(file_path))
|
|
252
|
+
else:
|
|
253
|
+
return os.path.abspath(file_path)
|
|
254
|
+
else:
|
|
255
|
+
return os.getcwd()
|
|
256
|
+
except Exception:
|
|
257
|
+
return os.getcwd()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def detect_project_root(file_path: Optional[str] = None,
|
|
261
|
+
explicit_root: Optional[str] = None) -> str:
|
|
262
|
+
"""
|
|
263
|
+
Unified project root detection with priority handling.
|
|
264
|
+
|
|
265
|
+
Priority order:
|
|
266
|
+
1. explicit_root parameter (highest priority)
|
|
267
|
+
2. Auto-detection from file_path
|
|
268
|
+
3. Auto-detection from current working directory
|
|
269
|
+
4. Fallback to file directory or cwd
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
file_path: Path to a file within the project
|
|
273
|
+
explicit_root: Explicitly specified project root
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Project root directory path
|
|
277
|
+
"""
|
|
278
|
+
detector = ProjectRootDetector()
|
|
279
|
+
|
|
280
|
+
# Priority 1: Explicit root
|
|
281
|
+
if explicit_root:
|
|
282
|
+
if os.path.exists(explicit_root) and os.path.isdir(explicit_root):
|
|
283
|
+
logger.info(f"Using explicit project root: {explicit_root}")
|
|
284
|
+
return os.path.abspath(explicit_root)
|
|
285
|
+
else:
|
|
286
|
+
logger.warning(f"Explicit project root does not exist: {explicit_root}")
|
|
287
|
+
|
|
288
|
+
# Priority 2: Auto-detection from file path
|
|
289
|
+
if file_path:
|
|
290
|
+
detected_root = detector.detect_from_file(file_path)
|
|
291
|
+
if detected_root:
|
|
292
|
+
logger.info(f"Auto-detected project root from file: {detected_root}")
|
|
293
|
+
return detected_root
|
|
294
|
+
|
|
295
|
+
# Priority 3: Auto-detection from cwd
|
|
296
|
+
detected_root = detector.detect_from_cwd()
|
|
297
|
+
if detected_root:
|
|
298
|
+
logger.info(f"Auto-detected project root from cwd: {detected_root}")
|
|
299
|
+
return detected_root
|
|
300
|
+
|
|
301
|
+
# Priority 4: Fallback
|
|
302
|
+
fallback_root = detector.get_fallback_root(file_path)
|
|
303
|
+
logger.info(f"Using fallback project root: {fallback_root}")
|
|
304
|
+
return fallback_root
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == "__main__":
|
|
308
|
+
# Test the detector
|
|
309
|
+
import sys
|
|
310
|
+
|
|
311
|
+
if len(sys.argv) > 1:
|
|
312
|
+
test_path = sys.argv[1]
|
|
313
|
+
result = detect_project_root(test_path)
|
|
314
|
+
print(f"Project root for '{test_path}': {result}")
|
|
315
|
+
else:
|
|
316
|
+
result = detect_project_root()
|
|
317
|
+
print(f"Project root from cwd: {result}")
|
|
@@ -76,13 +76,24 @@ class SecurityValidator:
|
|
|
76
76
|
log_warning(f"Null byte detected in file path: {file_path}")
|
|
77
77
|
return False, "File path contains null bytes"
|
|
78
78
|
|
|
79
|
-
# Layer 3: Windows drive letter check (
|
|
80
|
-
if len(file_path) > 1 and file_path[1] == ":":
|
|
81
|
-
return False, "Windows drive letters are not allowed"
|
|
79
|
+
# Layer 3: Windows drive letter check (only on non-Windows systems)
|
|
80
|
+
if len(file_path) > 1 and file_path[1] == ":" and os.name != 'nt':
|
|
81
|
+
return False, "Windows drive letters are not allowed on this system"
|
|
82
82
|
|
|
83
|
-
# Layer 4: Absolute path
|
|
83
|
+
# Layer 4: Absolute path check
|
|
84
84
|
if os.path.isabs(file_path):
|
|
85
|
-
|
|
85
|
+
# If we have a project root, check if the absolute path is within it
|
|
86
|
+
if self.boundary_manager and self.boundary_manager.project_root:
|
|
87
|
+
if not self.boundary_manager.is_within_project(file_path):
|
|
88
|
+
return False, "Absolute path must be within project directory"
|
|
89
|
+
else:
|
|
90
|
+
# In test environments (temp directories), allow absolute paths
|
|
91
|
+
import tempfile
|
|
92
|
+
temp_dir = tempfile.gettempdir()
|
|
93
|
+
if file_path.startswith(temp_dir):
|
|
94
|
+
return True, ""
|
|
95
|
+
# No project root defined, reject all other absolute paths
|
|
96
|
+
return False, "Absolute file paths are not allowed"
|
|
86
97
|
|
|
87
98
|
# Layer 5: Path normalization and traversal check
|
|
88
99
|
norm_path = os.path.normpath(file_path)
|
|
@@ -179,11 +190,17 @@ class SecurityValidator:
|
|
|
179
190
|
|
|
180
191
|
# Remove null bytes and control characters
|
|
181
192
|
sanitized = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', user_input)
|
|
182
|
-
|
|
193
|
+
|
|
194
|
+
# Remove HTML/XML tags for XSS prevention
|
|
195
|
+
sanitized = re.sub(r'<[^>]*>', '', sanitized)
|
|
196
|
+
|
|
197
|
+
# Remove potentially dangerous characters
|
|
198
|
+
sanitized = re.sub(r'[<>"\']', '', sanitized)
|
|
199
|
+
|
|
183
200
|
# Log if sanitization occurred
|
|
184
201
|
if sanitized != user_input:
|
|
185
202
|
log_warning("Input sanitization performed")
|
|
186
|
-
|
|
203
|
+
|
|
187
204
|
return sanitized
|
|
188
205
|
|
|
189
206
|
def validate_glob_pattern(self, pattern: str) -> Tuple[bool, str]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tree-sitter-analyzer
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: Extensible multi-language code analyzer framework using Tree-sitter with dynamic plugin architecture
|
|
5
5
|
Project-URL: Homepage, https://github.com/aimasteracc/tree-sitter-analyzer
|
|
6
6
|
Project-URL: Documentation, https://github.com/aimasteracc/tree-sitter-analyzer#readme
|
|
@@ -306,9 +306,32 @@ uv sync --extra all --extra mcp
|
|
|
306
306
|
|
|
307
307
|
- **[MCP Setup Guide for Users](https://github.com/aimasteracc/tree-sitter-analyzer/blob/main/MCP_SETUP_USERS.md)** - Simple setup for AI assistant users
|
|
308
308
|
- **[MCP Setup Guide for Developers](https://github.com/aimasteracc/tree-sitter-analyzer/blob/main/MCP_SETUP_DEVELOPERS.md)** - Local development configuration
|
|
309
|
+
- **[Project Root Configuration](https://github.com/aimasteracc/tree-sitter-analyzer/blob/main/PROJECT_ROOT_CONFIG.md)** - Complete configuration reference
|
|
309
310
|
- **[API Documentation](https://github.com/aimasteracc/tree-sitter-analyzer/blob/main/docs/api.md)** - Detailed API reference
|
|
310
311
|
- **[Contributing Guide](https://github.com/aimasteracc/tree-sitter-analyzer/blob/main/CONTRIBUTING.md)** - How to contribute
|
|
311
312
|
|
|
313
|
+
### 🔒 Project Root Configuration
|
|
314
|
+
|
|
315
|
+
Tree-sitter-analyzer automatically detects and secures your project boundaries:
|
|
316
|
+
|
|
317
|
+
- **Auto-detection**: Finds project root from `.git`, `pyproject.toml`, `package.json`, etc.
|
|
318
|
+
- **CLI**: Use `--project-root /path/to/project` for explicit control
|
|
319
|
+
- **MCP**: Set `TREE_SITTER_PROJECT_ROOT=${workspaceFolder}` for workspace integration
|
|
320
|
+
- **Security**: Only analyzes files within project boundaries
|
|
321
|
+
|
|
322
|
+
**Recommended MCP configuration:**
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"mcpServers": {
|
|
326
|
+
"tree-sitter-analyzer": {
|
|
327
|
+
"command": "uv",
|
|
328
|
+
"args": ["run", "--with", "tree-sitter-analyzer[mcp]", "python", "-m", "tree_sitter_analyzer.mcp.server"],
|
|
329
|
+
"env": {"TREE_SITTER_PROJECT_ROOT": "${workspaceFolder}"}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
312
335
|
## 🧪 Testing
|
|
313
336
|
|
|
314
337
|
This project maintains high code quality with **1126 passing tests**.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
tree_sitter_analyzer/__init__.py,sha256=T8urKvmHQaqEoh_-jKgNJOb2Snz2ySjMqYWE53vLKKA,3199
|
|
2
2
|
tree_sitter_analyzer/__main__.py,sha256=ilhMPpn_ar28oelzxLfQcX6WH_UbQ2euxiSoV3z_yCg,239
|
|
3
3
|
tree_sitter_analyzer/api.py,sha256=_94HoE1LKGELSE6FpZ6pEqm2R7qfoPokyfpGSjawliQ,17487
|
|
4
|
-
tree_sitter_analyzer/cli_main.py,sha256=
|
|
4
|
+
tree_sitter_analyzer/cli_main.py,sha256=ses68m5tLoYMP6Co3Fk2vqBACuFd38MqF85uEoa0mbw,9714
|
|
5
5
|
tree_sitter_analyzer/encoding_utils.py,sha256=C5DpH2-qkAKfsJeSGNHbhOCy4bmn46X6rUw5xPpki34,14938
|
|
6
6
|
tree_sitter_analyzer/exceptions.py,sha256=xO_U6JuJ4QPkmZoXL_3nmV9QUbTa7-hrI05VAuo5r-Y,12093
|
|
7
7
|
tree_sitter_analyzer/file_handler.py,sha256=vl4bGx-OgC6Lq63FEZNu2XCXNM0iDTmpNCRTK2msP3U,7104
|
|
@@ -9,23 +9,24 @@ tree_sitter_analyzer/language_detector.py,sha256=IjkYF1E7_TtWlwYjz780ZUJAyPltL2a
|
|
|
9
9
|
tree_sitter_analyzer/language_loader.py,sha256=gdLxkSoajm-q7c1vcvFONtBf5XJRgasUVI4L0wMzra0,8124
|
|
10
10
|
tree_sitter_analyzer/models.py,sha256=z0aqdZOVA8rYWF0143TSAUoCvncVRLZ1O70eAjV87gU,16564
|
|
11
11
|
tree_sitter_analyzer/output_manager.py,sha256=eiBOSL2vUUQi1ghYBr4gwT7aOYC2WTgIoISBZlXkzPo,8399
|
|
12
|
+
tree_sitter_analyzer/project_detector.py,sha256=tB5giHVa_jvPv44l0gv7u265Ih28Fw2ADKUEkb1YFnk,9565
|
|
12
13
|
tree_sitter_analyzer/query_loader.py,sha256=NilC2XmmhYrBL6ONlzRGlehGa23C_4V6nDVap6YG8v0,10120
|
|
13
14
|
tree_sitter_analyzer/table_formatter.py,sha256=BfrAouAr3r6MD9xY9yhHw_PwD0aJ4BQo5p1UFhorT5k,27284
|
|
14
15
|
tree_sitter_analyzer/utils.py,sha256=Pq_2vlDPul8jean0PwlQ_XC-RDjkuaUbwoXp2ls7dV8,8268
|
|
15
16
|
tree_sitter_analyzer/cli/__init__.py,sha256=swCjWlrPEVIKGznqM_BPxbNvd_0Qz5r1_RmZ-j6EWIU,910
|
|
16
17
|
tree_sitter_analyzer/cli/__main__.py,sha256=xgCuvLv5NNeEsxKM40pF_7b1apgj3DZ4ECa-xcbLKWc,230
|
|
17
|
-
tree_sitter_analyzer/cli/info_commands.py,sha256=
|
|
18
|
+
tree_sitter_analyzer/cli/info_commands.py,sha256=0x_6mfMq7jpKBLT9jzhTikXcs0n4TzNEV2Te9dyKNd4,4405
|
|
18
19
|
tree_sitter_analyzer/cli/commands/__init__.py,sha256=qLtJ7rRge-Reu4aZbczn_jmUHQNQ4lEAsve9BZYHYd0,697
|
|
19
20
|
tree_sitter_analyzer/cli/commands/advanced_command.py,sha256=YJGrFBEqFPpS0VB-o28Un89Cjwr-eTirNdcFLP4rlN8,3512
|
|
20
|
-
tree_sitter_analyzer/cli/commands/base_command.py,sha256=
|
|
21
|
-
tree_sitter_analyzer/cli/commands/default_command.py,sha256
|
|
22
|
-
tree_sitter_analyzer/cli/commands/partial_read_command.py,sha256=
|
|
23
|
-
tree_sitter_analyzer/cli/commands/query_command.py,sha256=
|
|
21
|
+
tree_sitter_analyzer/cli/commands/base_command.py,sha256=0CyODjCOWahH2x-PdeirxrKJMBNzTeRkfPvPuimhIXA,6770
|
|
22
|
+
tree_sitter_analyzer/cli/commands/default_command.py,sha256=R9_GuI5KVYPK2DfXRuG8L89vwxv0QVW8sur_sigjZKo,542
|
|
23
|
+
tree_sitter_analyzer/cli/commands/partial_read_command.py,sha256=kD3E2f1zCseSKpGQ3bgHnEuCq-DCPRQrT91JJJh8B4Q,4776
|
|
24
|
+
tree_sitter_analyzer/cli/commands/query_command.py,sha256=5GlctGaJIc_3AGdISMNFJkbGB3hx6YixYBnKK8VmsF0,3647
|
|
24
25
|
tree_sitter_analyzer/cli/commands/structure_command.py,sha256=u-NKm06CLgx4srdK5bVo7WtcV4dArA7WYWQWmeXcWMs,5358
|
|
25
26
|
tree_sitter_analyzer/cli/commands/summary_command.py,sha256=X3pLK7t2ma4SDlG7yYsaFX6bQ4OVUrHv8OWDfgTMNMw,3703
|
|
26
|
-
tree_sitter_analyzer/cli/commands/table_command.py,sha256=
|
|
27
|
+
tree_sitter_analyzer/cli/commands/table_command.py,sha256=BAIw26WRi_yXbKvkuV7tXFKzSiWvYKVzRUxAcgsJ7VQ,9676
|
|
27
28
|
tree_sitter_analyzer/core/__init__.py,sha256=Um_BRFICWihZybxoAR6Ck32gJ42ZatkBoZR18XGl9FQ,455
|
|
28
|
-
tree_sitter_analyzer/core/analysis_engine.py,sha256=
|
|
29
|
+
tree_sitter_analyzer/core/analysis_engine.py,sha256=Hp72NvmnWduDI1bn7ArKOT1ho9o2_0RZqj_lU_UMqjQ,20493
|
|
29
30
|
tree_sitter_analyzer/core/cache_service.py,sha256=lTFhGsmuGWgEauxtk2pz_1h5Z945456CaQILfreS5Rw,9944
|
|
30
31
|
tree_sitter_analyzer/core/engine.py,sha256=6NRPBlN1GvFDtSh8hDJ8udKLC7IOjvoCPetuM7MPrnw,19287
|
|
31
32
|
tree_sitter_analyzer/core/parser.py,sha256=07vL-mESeMsaIrQqAg-3sr9MLWYVdzT5RyBDO1AkBh0,9586
|
|
@@ -45,17 +46,17 @@ tree_sitter_analyzer/languages/java_plugin.py,sha256=o_9F_anKCemnUDV6hq28RatRmBm
|
|
|
45
46
|
tree_sitter_analyzer/languages/javascript_plugin.py,sha256=9al0ScXmM5Y8Xl82oNp7cUaU9P59eNCJCPXSlfea4u8,16290
|
|
46
47
|
tree_sitter_analyzer/languages/python_plugin.py,sha256=nlVxDx6thOB5o6QfQzGbD7gph3_YuM32YYzqYZoHlMw,29899
|
|
47
48
|
tree_sitter_analyzer/mcp/__init__.py,sha256=mL_XjEks3tJOGAl9ULs_09KQOH1BWi92yvXpBidwmlI,752
|
|
48
|
-
tree_sitter_analyzer/mcp/server.py,sha256=
|
|
49
|
+
tree_sitter_analyzer/mcp/server.py,sha256=7p5LdihdLMl0BH_u6vGk1VDLTiycLzAOeBsxaug4LrY,15669
|
|
49
50
|
tree_sitter_analyzer/mcp/resources/__init__.py,sha256=PHDvZyHZawoToDQVqrepsmcTk00ZlaTsu6uxwVjoa4A,1433
|
|
50
51
|
tree_sitter_analyzer/mcp/resources/code_file_resource.py,sha256=MDHvJl6akElHtcxlN6eCcY5WYSjQEQFCyhAVGiPGk9s,6462
|
|
51
52
|
tree_sitter_analyzer/mcp/resources/project_stats_resource.py,sha256=lZF9TGxjKvTwPyuWE_o3I3V4LK0zEj3lab4L0Iq-hho,19758
|
|
52
53
|
tree_sitter_analyzer/mcp/tools/__init__.py,sha256=RMvJOzfZMVe24WUNWJJ-pdygc1RbEVrhW5NZwpykDoQ,792
|
|
53
|
-
tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py,sha256=
|
|
54
|
+
tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py,sha256=yI33yev1W-MztyjiPlSX4uwCcFigRpzdHloXNCXAQz8,27938
|
|
54
55
|
tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py,sha256=Ie1yeGTFNxuEeTLgXVnKEdKktoMEV27ychIMVkStRY8,9244
|
|
55
56
|
tree_sitter_analyzer/mcp/tools/base_tool.py,sha256=szW84sSYejzRyBlFbskOARQbsfc2JLwHmjZ6rJZ8SQA,1264
|
|
56
|
-
tree_sitter_analyzer/mcp/tools/read_partial_tool.py,sha256=
|
|
57
|
-
tree_sitter_analyzer/mcp/tools/table_format_tool.py,sha256=
|
|
58
|
-
tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py,sha256=
|
|
57
|
+
tree_sitter_analyzer/mcp/tools/read_partial_tool.py,sha256=Hjfl1-b0BVsT-g6zr0-pxXA0T1tKaE0iLJZFMm-fxRI,11505
|
|
58
|
+
tree_sitter_analyzer/mcp/tools/table_format_tool.py,sha256=JUfkB32ZXf-RQ5O2nKC2jFTVR1AxD8lks5vjjDFEoNw,15502
|
|
59
|
+
tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py,sha256=MbJEzWa0b2KtHLIgmy5WVcCN89YL4tB1drujoHt9axs,22173
|
|
59
60
|
tree_sitter_analyzer/mcp/utils/__init__.py,sha256=F_qFFC2gvGNdgRWGLxIh4Amd0dPhZv0Ni1ZbCbaYLlI,3063
|
|
60
61
|
tree_sitter_analyzer/mcp/utils/error_handler.py,sha256=n9ME2U5L1o65Vewnv8kD2O8dVI1CiEGm1pLWdnpnyqM,17972
|
|
61
62
|
tree_sitter_analyzer/plugins/__init__.py,sha256=MfSW8P9GLaL_9XgLISdlpIUY4quqapk0avPLIpBdMTg,10606
|
|
@@ -69,8 +70,8 @@ tree_sitter_analyzer/queries/typescript.py,sha256=I1ndwPjAMGOIa1frSK3ewLqEkeDAJu
|
|
|
69
70
|
tree_sitter_analyzer/security/__init__.py,sha256=zVpzS5jtECwgYnhKL4YoMfnIdkJABnVeziTBB4IOTyU,624
|
|
70
71
|
tree_sitter_analyzer/security/boundary_manager.py,sha256=e4iOJTygHLqlImkOntjLhfTpCvqCfb2LTpYwGpYmVQg,8051
|
|
71
72
|
tree_sitter_analyzer/security/regex_checker.py,sha256=Qvldh-TiVYqtcYQbD80wk0eHUvhALYtWTWBy_bGmJUk,10025
|
|
72
|
-
tree_sitter_analyzer/security/validator.py,sha256=
|
|
73
|
-
tree_sitter_analyzer-0.8.
|
|
74
|
-
tree_sitter_analyzer-0.8.
|
|
75
|
-
tree_sitter_analyzer-0.8.
|
|
76
|
-
tree_sitter_analyzer-0.8.
|
|
73
|
+
tree_sitter_analyzer/security/validator.py,sha256=yL72kKnMN2IeqJk3i9l9FpWP9_Mt4lPErpj8ys5J_WY,9118
|
|
74
|
+
tree_sitter_analyzer-0.8.1.dist-info/METADATA,sha256=DGfahyUO9cgkNyyOUtwQrew4UtZbTSpanuhAivGRSN8,14422
|
|
75
|
+
tree_sitter_analyzer-0.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
76
|
+
tree_sitter_analyzer-0.8.1.dist-info/entry_points.txt,sha256=EA0Ow27x2SqNt2300sv70RTWxKRIxJzOhNPIVlez4NM,417
|
|
77
|
+
tree_sitter_analyzer-0.8.1.dist-info/RECORD,,
|
|
File without changes
|
{tree_sitter_analyzer-0.8.0.dist-info → tree_sitter_analyzer-0.8.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|