tree-sitter-analyzer 1.6.1.2__py3-none-any.whl → 1.6.1.3__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__ = "1.6.1.2"
14
+ __version__ = "1.6.1.3"
15
15
  __author__ = "aisheng.yu"
16
16
  __email__ = "aimasteracc@gmail.com"
17
17
 
@@ -10,7 +10,7 @@ import logging
10
10
  import time
11
11
  from typing import Any
12
12
 
13
- from tree_sitter import Language, Node, Tree
13
+ from tree_sitter import Language, Node, Query, QueryCursor, Tree
14
14
 
15
15
  from ..query_loader import get_query_loader
16
16
 
@@ -77,10 +77,11 @@ class QueryExecutor:
77
77
  f"Query '{query_name}' not found", query_name=query_name
78
78
  )
79
79
 
80
- # Create and execute the query
80
+ # Create and execute the query using new API (tree-sitter 0.25.0+)
81
81
  try:
82
- query = language.query(query_string)
83
- captures = query.captures(tree.root_node)
82
+ query = Query(language, query_string)
83
+ cursor = QueryCursor(query)
84
+ captures = list(cursor.captures(tree.root_node))
84
85
 
85
86
  # Process captures
86
87
  try:
@@ -146,10 +147,11 @@ class QueryExecutor:
146
147
  if language is None:
147
148
  return self._create_error_result("Language is None") # type: ignore[unreachable]
148
149
 
149
- # Create and execute the query
150
+ # Create and execute the query using new API (tree-sitter 0.25.0+)
150
151
  try:
151
- query = language.query(query_string)
152
- captures = query.captures(tree.root_node)
152
+ query = Query(language, query_string)
153
+ cursor = QueryCursor(query)
154
+ captures = list(cursor.captures(tree.root_node))
153
155
 
154
156
  # Process captures
155
157
  try:
@@ -373,8 +375,8 @@ class QueryExecutor:
373
375
  if lang_obj is None:
374
376
  return False
375
377
 
376
- # Try to create the query
377
- lang_obj.query(query_string)
378
+ # Try to create the query using new API (tree-sitter 0.25.0+)
379
+ Query(lang_obj, query_string)
378
380
  return True
379
381
 
380
382
  except Exception as e:
@@ -9,6 +9,8 @@ Provides core tree-sitter query functionality including predefined and custom qu
9
9
  import logging
10
10
  from typing import Any
11
11
 
12
+ from tree_sitter import Query, QueryCursor
13
+
12
14
  from ..encoding_utils import read_file_safe
13
15
  from ..query_loader import query_loader
14
16
  from .parser import Parser
@@ -80,23 +82,18 @@ class QueryService:
80
82
  f"Query '{query_key}' not found for language '{language}'"
81
83
  )
82
84
 
83
- # Execute tree-sitter query
84
- ts_query = language_obj.query(query_string)
85
- captures = ts_query.captures(tree.root_node)
85
+ # Execute tree-sitter query using new API (tree-sitter 0.25.0+)
86
+ ts_query = Query(language_obj, query_string)
87
+ cursor = QueryCursor(ts_query)
88
+ matches = cursor.matches(tree.root_node)
86
89
 
87
- # Process capture results
90
+ # Process match results (new API returns list of (pattern_index, captures_dict))
88
91
  results = []
89
- if isinstance(captures, dict):
90
- # New tree-sitter API returns dictionary
91
- for capture_name, nodes in captures.items():
92
+ for pattern_index, captures_dict in matches:
93
+ # captures_dict is {capture_name: [node1, node2, ...]}
94
+ for capture_name, nodes in captures_dict.items():
92
95
  for node in nodes:
93
96
  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
97
 
101
98
  # Apply filters
102
99
  if filter_expression and results:
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
15
15
 
16
16
  try:
17
17
  import tree_sitter
18
+ from tree_sitter import Query, QueryCursor
18
19
 
19
20
  TREE_SITTER_AVAILABLE = True
20
21
  except ImportError:
@@ -113,17 +114,23 @@ class PythonElementExtractor(ElementExtractor):
113
114
 
114
115
  language = tree.language if hasattr(tree, "language") else None
115
116
  if language:
116
- query = language.query(class_query)
117
- captures = query.captures(tree.root_node)
118
-
119
- if isinstance(captures, dict):
120
- class_bodies = captures.get("class.body", [])
121
-
122
- # For each class body, extract attribute assignments
123
- for class_body in class_bodies:
124
- variables.extend(
125
- self._extract_class_attributes(class_body, source_code)
126
- )
117
+ # Use new API (tree-sitter 0.25.0+)
118
+ query = Query(language, class_query)
119
+ cursor = QueryCursor(query)
120
+ matches = cursor.matches(tree.root_node)
121
+
122
+ # Process matches to get class bodies
123
+ class_bodies = []
124
+ for pattern_index, captures_dict in matches:
125
+ for capture_name, nodes in captures_dict.items():
126
+ if capture_name == "class.body":
127
+ class_bodies.extend(nodes)
128
+
129
+ # For each class body, extract attribute assignments
130
+ for class_body in class_bodies:
131
+ variables.extend(
132
+ self._extract_class_attributes(class_body, source_code)
133
+ )
127
134
 
128
135
  except Exception as e:
129
136
  log_warning(f"Could not extract Python class attributes: {e}")
@@ -664,20 +671,29 @@ class PythonElementExtractor(ElementExtractor):
664
671
  language = tree.language if hasattr(tree, "language") else None
665
672
  if language:
666
673
  for query_string in import_queries:
667
- query = language.query(query_string)
668
- captures = query.captures(tree.root_node)
669
-
670
- if isinstance(captures, dict):
671
- # Process different types of imports
672
- for key, nodes in captures.items():
673
- if key.endswith("statement"):
674
- import_type = key.split(".")[0]
675
- for node in nodes:
676
- imp = self._extract_import_info(
677
- node, source_code, import_type
678
- )
679
- if imp:
680
- imports.append(imp)
674
+ # Use new API (tree-sitter 0.25.0+)
675
+ query = Query(language, query_string)
676
+ cursor = QueryCursor(query)
677
+ matches = cursor.matches(tree.root_node)
678
+
679
+ # Process matches to get statement nodes
680
+ statement_nodes = {}
681
+ for pattern_index, captures_dict in matches:
682
+ for capture_name, nodes in captures_dict.items():
683
+ if capture_name.endswith("statement"):
684
+ import_type = capture_name.split(".")[0]
685
+ if import_type not in statement_nodes:
686
+ statement_nodes[import_type] = []
687
+ statement_nodes[import_type].extend(nodes)
688
+
689
+ # Process different types of imports
690
+ for import_type, nodes in statement_nodes.items():
691
+ for node in nodes:
692
+ imp = self._extract_import_info(
693
+ node, source_code, import_type
694
+ )
695
+ if imp:
696
+ imports.append(imp)
681
697
 
682
698
  except Exception as e:
683
699
  log_warning(f"Could not extract Python imports: {e}")
@@ -1179,8 +1195,16 @@ class PythonPlugin(LanguagePlugin):
1179
1195
  else:
1180
1196
  return {"error": f"Unknown query: {query_name}"}
1181
1197
 
1182
- query = language.query(query_string)
1183
- captures = query.captures(tree.root_node)
1198
+ # Use new API (tree-sitter 0.25.0+)
1199
+ query = Query(language, query_string)
1200
+ cursor = QueryCursor(query)
1201
+ matches = list(cursor.matches(tree.root_node))
1202
+ # Convert matches to legacy format for compatibility
1203
+ captures = []
1204
+ for pattern_index, captures_dict in matches:
1205
+ for capture_name, nodes in captures_dict.items():
1206
+ for node in nodes:
1207
+ captures.append((node, capture_name))
1184
1208
  return {"captures": captures, "query": query_string}
1185
1209
 
1186
1210
  except Exception as e:
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 統一ログ管理システム
4
+
5
+ ログ出力の重複問題を解決するためのLoggerManagerクラスを提供します。
6
+ シングルトンパターンによりロガーインスタンスを一意に管理し、
7
+ 重複ハンドラーを防止します。
8
+ """
9
+
10
+ import logging
11
+ import os
12
+ import sys
13
+ import tempfile
14
+ import threading
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional
17
+
18
+
19
+ class LoggerManager:
20
+ """
21
+ 統一されたロガー管理クラス
22
+
23
+ シングルトンパターンでロガーインスタンスを管理し、
24
+ 重複ハンドラーを防止する。
25
+ """
26
+
27
+ _instance: Optional['LoggerManager'] = None
28
+ _lock: threading.Lock = threading.Lock()
29
+ _loggers: Dict[str, logging.Logger] = {}
30
+ _handler_registry: Dict[str, List[str]] = {}
31
+ _initialized: bool = False
32
+ _file_log_message_shown: bool = False
33
+
34
+ def __new__(cls) -> 'LoggerManager':
35
+ """スレッドセーフなシングルトン実装"""
36
+ if cls._instance is None:
37
+ with cls._lock:
38
+ if cls._instance is None:
39
+ cls._instance = super().__new__(cls)
40
+ return cls._instance
41
+
42
+ def __init__(self) -> None:
43
+ """初期化(シングルトンのため一度のみ実行)"""
44
+ if not self._initialized:
45
+ with self._lock:
46
+ if not self._initialized:
47
+ self._loggers = {}
48
+ self._handler_registry = {}
49
+ self._initialized = True
50
+
51
+ def get_logger(
52
+ self,
53
+ name: str = "tree_sitter_analyzer",
54
+ level: int | str = logging.WARNING
55
+ ) -> logging.Logger:
56
+ """
57
+ 重複を防ぐロガー取得
58
+
59
+ Args:
60
+ name: ロガー名
61
+ level: ログレベル
62
+
63
+ Returns:
64
+ 設定済みロガーインスタンス
65
+ """
66
+ with self._lock:
67
+ if name not in self._loggers:
68
+ self._loggers[name] = self._create_logger(name, level)
69
+ else:
70
+ # 既存のロガーでもレベルを更新
71
+ numeric_level = self._convert_level(level)
72
+
73
+ # 環境変数からのレベル設定が優先
74
+ env_level = os.environ.get("LOG_LEVEL", "").upper()
75
+ if env_level and env_level in ["DEBUG", "INFO", "WARNING", "ERROR"]:
76
+ numeric_level = getattr(logging, env_level)
77
+
78
+ self._loggers[name].setLevel(numeric_level)
79
+
80
+ return self._loggers[name]
81
+
82
+ def _create_logger(self, name: str, level: int | str) -> logging.Logger:
83
+ """
84
+ ロガー作成とハンドラー設定
85
+
86
+ Args:
87
+ name: ロガー名
88
+ level: ログレベル
89
+
90
+ Returns:
91
+ 設定済みロガーインスタンス
92
+ """
93
+ # レベル変換処理
94
+ numeric_level = self._convert_level(level)
95
+
96
+ # 環境変数からのレベル設定
97
+ env_level = os.environ.get("LOG_LEVEL", "").upper()
98
+ if env_level and env_level in ["DEBUG", "INFO", "WARNING", "ERROR"]:
99
+ numeric_level = getattr(logging, env_level)
100
+
101
+ logger = logging.getLogger(name)
102
+
103
+ # 重複ハンドラーチェック
104
+ if not self._has_required_handlers(logger, name):
105
+ self._setup_handlers(logger, name, numeric_level)
106
+
107
+ # ロガーレベル設定
108
+ logger.setLevel(numeric_level)
109
+
110
+ return logger
111
+
112
+ def _convert_level(self, level: int | str) -> int:
113
+ """ログレベル文字列を数値に変換"""
114
+ if isinstance(level, str):
115
+ level_upper = level.upper()
116
+ level_map = {
117
+ "DEBUG": logging.DEBUG,
118
+ "INFO": logging.INFO,
119
+ "WARNING": logging.WARNING,
120
+ "ERROR": logging.ERROR,
121
+ }
122
+ return level_map.get(level_upper, logging.WARNING)
123
+ return level
124
+
125
+ def _has_required_handlers(self, logger: logging.Logger, name: str) -> bool:
126
+ """
127
+ 必要なハンドラーが既に設定されているかチェック
128
+
129
+ Args:
130
+ logger: チェック対象ロガー
131
+ name: ロガー名
132
+
133
+ Returns:
134
+ 必要なハンドラーが設定済みの場合True
135
+ """
136
+ if name in self._handler_registry:
137
+ # 既に管理されているロガーの場合は設定済みとみなす
138
+ return True
139
+
140
+ # 既存ハンドラーの有無をチェック
141
+ has_stream_handler = any(
142
+ isinstance(h, logging.StreamHandler) and not isinstance(h, logging.FileHandler)
143
+ for h in logger.handlers
144
+ )
145
+
146
+ if has_stream_handler:
147
+ # ハンドラー登録を記録
148
+ handler_types = [type(h).__name__ for h in logger.handlers]
149
+ self._handler_registry[name] = handler_types
150
+ return True
151
+
152
+ return False
153
+
154
+ def _setup_handlers(self, logger: logging.Logger, name: str, level: int) -> None:
155
+ """
156
+ ロガーにハンドラーを設定
157
+
158
+ Args:
159
+ logger: 設定対象ロガー
160
+ name: ロガー名
161
+ level: ログレベル
162
+ """
163
+ # メインハンドラー(stderr)の追加
164
+ if not self._has_stream_handler(logger):
165
+ stream_handler = SafeStreamHandler()
166
+ formatter = logging.Formatter(
167
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
168
+ )
169
+ stream_handler.setFormatter(formatter)
170
+ logger.addHandler(stream_handler)
171
+
172
+ # ファイルログハンドラーの追加(環境変数で有効化)
173
+ enable_file_log = (
174
+ os.environ.get("TREE_SITTER_ANALYZER_ENABLE_FILE_LOG", "").lower() == "true"
175
+ )
176
+
177
+ if enable_file_log and not self._has_file_handler(logger):
178
+ file_handler = self._create_file_handler(level)
179
+ if file_handler:
180
+ logger.addHandler(file_handler)
181
+
182
+ # ハンドラー登録を記録
183
+ handler_types = [type(h).__name__ for h in logger.handlers]
184
+ self._handler_registry[name] = handler_types
185
+
186
+ def _has_stream_handler(self, logger: logging.Logger) -> bool:
187
+ """StreamHandlerの存在チェック"""
188
+ return any(
189
+ isinstance(h, logging.StreamHandler) and not isinstance(h, logging.FileHandler)
190
+ for h in logger.handlers
191
+ )
192
+
193
+ def _has_file_handler(self, logger: logging.Logger) -> bool:
194
+ """FileHandlerの存在チェック"""
195
+ return any(isinstance(h, logging.FileHandler) for h in logger.handlers)
196
+
197
+ def _create_file_handler(self, level: int) -> Optional[logging.FileHandler]:
198
+ """
199
+ ファイルハンドラーの作成
200
+
201
+ Args:
202
+ level: ログレベル
203
+
204
+ Returns:
205
+ 作成されたFileHandlerまたはNone
206
+ """
207
+ try:
208
+ # ログディレクトリの決定
209
+ log_dir = os.environ.get("TREE_SITTER_ANALYZER_LOG_DIR")
210
+ if log_dir:
211
+ log_path = Path(log_dir) / "tree_sitter_analyzer.log"
212
+ Path(log_dir).mkdir(parents=True, exist_ok=True)
213
+ else:
214
+ temp_dir = tempfile.gettempdir()
215
+ log_path = Path(temp_dir) / "tree_sitter_analyzer.log"
216
+
217
+ # ファイルログレベルの決定
218
+ file_log_level_str = os.environ.get(
219
+ "TREE_SITTER_ANALYZER_FILE_LOG_LEVEL", ""
220
+ ).upper()
221
+ file_log_level = level # デフォルトはメインレベル
222
+
223
+ if file_log_level_str in ["DEBUG", "INFO", "WARNING", "ERROR"]:
224
+ file_log_level = getattr(logging, file_log_level_str)
225
+
226
+ # ファイルハンドラー作成
227
+ file_handler = logging.FileHandler(str(log_path), encoding="utf-8")
228
+ formatter = logging.Formatter(
229
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
230
+ )
231
+ file_handler.setFormatter(formatter)
232
+ file_handler.setLevel(file_log_level)
233
+
234
+ # ファイルパス情報を出力(1回のみ)
235
+ if not LoggerManager._file_log_message_shown:
236
+ LoggerManager._file_log_message_shown = True
237
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
238
+ try:
239
+ sys.stderr.write(
240
+ f"[LoggerManager] File logging enabled: {log_path}\n"
241
+ )
242
+ except Exception:
243
+ pass
244
+
245
+ return file_handler
246
+
247
+ except Exception as e:
248
+ # ファイルハンドラー作成に失敗してもメインの動作は継続
249
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
250
+ try:
251
+ sys.stderr.write(
252
+ f"[LoggerManager] File handler creation failed: {e}\n"
253
+ )
254
+ except Exception:
255
+ pass
256
+ return None
257
+
258
+ def reset_for_testing(self) -> None:
259
+ """
260
+ テスト用リセット機能
261
+
262
+ Note:
263
+ 本番環境では使用しないこと
264
+ """
265
+ with self._lock:
266
+ # 全ハンドラーのクリーンアップ
267
+ for logger in self._loggers.values():
268
+ for handler in logger.handlers[:]:
269
+ try:
270
+ handler.close()
271
+ logger.removeHandler(handler)
272
+ except Exception:
273
+ pass
274
+
275
+ self._loggers.clear()
276
+ self._handler_registry.clear()
277
+ LoggerManager._file_log_message_shown = False
278
+
279
+
280
+ class SafeStreamHandler(logging.StreamHandler):
281
+ """
282
+ 安全なStreamHandler実装
283
+
284
+ MCPプロトコルのstdio通信やテスト環境での
285
+ ストリームクローズ問題に対応。
286
+ """
287
+
288
+ def __init__(self, stream=None):
289
+ # デフォルトでstderrを使用(stdoutはMCP用に保持)
290
+ super().__init__(stream if stream is not None else sys.stderr)
291
+
292
+ def emit(self, record: Any) -> None:
293
+ """
294
+ レコードの安全な出力
295
+
296
+ Args:
297
+ record: ログレコード
298
+ """
299
+ try:
300
+ # ストリームの状態チェック
301
+ if hasattr(self.stream, "closed") and self.stream.closed:
302
+ return
303
+
304
+ if not hasattr(self.stream, "write"):
305
+ return
306
+
307
+ # pytest環境での特別処理
308
+ stream_name = getattr(self.stream, "name", "")
309
+ if stream_name is None or "pytest" in str(type(self.stream)).lower():
310
+ try:
311
+ super().emit(record)
312
+ return
313
+ except (ValueError, OSError, AttributeError, UnicodeError):
314
+ return
315
+
316
+ # 通常のストリーム書き込み可能性チェック
317
+ try:
318
+ if hasattr(self.stream, "writable") and not self.stream.writable():
319
+ return
320
+ except (ValueError, OSError, AttributeError, UnicodeError):
321
+ return
322
+
323
+ super().emit(record)
324
+
325
+ except (ValueError, OSError, AttributeError, UnicodeError):
326
+ # I/Oエラーは静かに無視(シャットダウン時やpytestキャプチャ時)
327
+ pass
328
+ except Exception:
329
+ # その他の予期しないエラーも静かに無視
330
+ pass
331
+
332
+
333
+ # グローバルインスタンス
334
+ _logger_manager = LoggerManager()
335
+
336
+
337
+ def get_logger_manager() -> LoggerManager:
338
+ """
339
+ LoggerManagerのグローバルインスタンス取得
340
+
341
+ Returns:
342
+ LoggerManagerインスタンス
343
+ """
344
+ return _logger_manager
345
+
346
+
347
+ def get_unified_logger(
348
+ name: str = "tree_sitter_analyzer",
349
+ level: int | str = logging.WARNING
350
+ ) -> logging.Logger:
351
+ """
352
+ 統一されたロガー取得関数
353
+
354
+ Args:
355
+ name: ロガー名
356
+ level: ログレベル
357
+
358
+ Returns:
359
+ 設定済みロガーインスタンス
360
+ """
361
+ return _logger_manager.get_logger(name, level)
@@ -67,7 +67,7 @@ from .tools.read_partial_tool import ReadPartialTool
67
67
  from .tools.search_content_tool import SearchContentTool
68
68
  from .tools.table_format_tool import TableFormatTool
69
69
 
70
- # Set up logging
70
+ # Set up logging using unified LoggerManager
71
71
  logger = setup_logger(__name__)
72
72
 
73
73