tree-sitter-analyzer 1.9.2__py3-none-any.whl → 1.9.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tree-sitter-analyzer might be problematic. Click here for more details.

Files changed (64) hide show
  1. tree_sitter_analyzer/__init__.py +1 -1
  2. tree_sitter_analyzer/api.py +216 -8
  3. tree_sitter_analyzer/cli/argument_validator.py +1 -1
  4. tree_sitter_analyzer/cli/commands/advanced_command.py +3 -6
  5. tree_sitter_analyzer/cli/commands/query_command.py +3 -1
  6. tree_sitter_analyzer/cli/commands/table_command.py +3 -3
  7. tree_sitter_analyzer/constants.py +5 -3
  8. tree_sitter_analyzer/core/analysis_engine.py +1 -1
  9. tree_sitter_analyzer/core/cache_service.py +1 -1
  10. tree_sitter_analyzer/core/engine.py +34 -10
  11. tree_sitter_analyzer/core/query.py +82 -2
  12. tree_sitter_analyzer/encoding_utils.py +64 -0
  13. tree_sitter_analyzer/exceptions.py +1 -1
  14. tree_sitter_analyzer/file_handler.py +49 -33
  15. tree_sitter_analyzer/formatters/base_formatter.py +1 -1
  16. tree_sitter_analyzer/formatters/html_formatter.py +24 -14
  17. tree_sitter_analyzer/formatters/javascript_formatter.py +28 -21
  18. tree_sitter_analyzer/formatters/language_formatter_factory.py +7 -4
  19. tree_sitter_analyzer/formatters/markdown_formatter.py +4 -4
  20. tree_sitter_analyzer/formatters/python_formatter.py +4 -4
  21. tree_sitter_analyzer/formatters/typescript_formatter.py +1 -1
  22. tree_sitter_analyzer/interfaces/mcp_adapter.py +4 -2
  23. tree_sitter_analyzer/interfaces/mcp_server.py +10 -10
  24. tree_sitter_analyzer/language_detector.py +30 -5
  25. tree_sitter_analyzer/language_loader.py +46 -26
  26. tree_sitter_analyzer/languages/css_plugin.py +6 -6
  27. tree_sitter_analyzer/languages/html_plugin.py +12 -8
  28. tree_sitter_analyzer/languages/java_plugin.py +330 -520
  29. tree_sitter_analyzer/languages/javascript_plugin.py +22 -78
  30. tree_sitter_analyzer/languages/markdown_plugin.py +277 -297
  31. tree_sitter_analyzer/languages/python_plugin.py +47 -85
  32. tree_sitter_analyzer/languages/typescript_plugin.py +48 -123
  33. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +14 -8
  34. tree_sitter_analyzer/mcp/server.py +38 -23
  35. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +10 -7
  36. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +51 -7
  37. tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +11 -7
  38. tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +8 -6
  39. tree_sitter_analyzer/mcp/tools/list_files_tool.py +6 -6
  40. tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
  41. tree_sitter_analyzer/mcp/tools/search_content_tool.py +48 -15
  42. tree_sitter_analyzer/mcp/tools/table_format_tool.py +13 -8
  43. tree_sitter_analyzer/mcp/utils/file_output_manager.py +8 -3
  44. tree_sitter_analyzer/mcp/utils/gitignore_detector.py +24 -12
  45. tree_sitter_analyzer/mcp/utils/path_resolver.py +2 -2
  46. tree_sitter_analyzer/models.py +16 -0
  47. tree_sitter_analyzer/mypy_current_errors.txt +2 -0
  48. tree_sitter_analyzer/plugins/base.py +66 -0
  49. tree_sitter_analyzer/queries/java.py +9 -3
  50. tree_sitter_analyzer/queries/javascript.py +3 -8
  51. tree_sitter_analyzer/queries/markdown.py +1 -1
  52. tree_sitter_analyzer/queries/python.py +2 -2
  53. tree_sitter_analyzer/security/boundary_manager.py +2 -5
  54. tree_sitter_analyzer/security/regex_checker.py +2 -2
  55. tree_sitter_analyzer/security/validator.py +5 -1
  56. tree_sitter_analyzer/table_formatter.py +4 -4
  57. tree_sitter_analyzer/utils/__init__.py +27 -116
  58. tree_sitter_analyzer/{utils.py → utils/logging.py} +2 -2
  59. tree_sitter_analyzer/utils/tree_sitter_compat.py +2 -2
  60. {tree_sitter_analyzer-1.9.2.dist-info → tree_sitter_analyzer-1.9.4.dist-info}/METADATA +87 -45
  61. tree_sitter_analyzer-1.9.4.dist-info/RECORD +111 -0
  62. tree_sitter_analyzer-1.9.2.dist-info/RECORD +0 -109
  63. {tree_sitter_analyzer-1.9.2.dist-info → tree_sitter_analyzer-1.9.4.dist-info}/WHEEL +0 -0
  64. {tree_sitter_analyzer-1.9.2.dist-info → tree_sitter_analyzer-1.9.4.dist-info}/entry_points.txt +0 -0
@@ -374,8 +374,9 @@ class TableFormatTool(BaseMCPTool):
374
374
 
375
375
  # Write content to file
376
376
  try:
377
- with open(output_path, "w", encoding="utf-8") as f:
378
- f.write(content)
377
+ from ...encoding_utils import write_file_safe
378
+
379
+ write_file_safe(output_path, content)
379
380
  self.logger.info(f"Output written to file: {output_path}")
380
381
  return output_path
381
382
  except Exception as e:
@@ -471,19 +472,23 @@ class TableFormatTool(BaseMCPTool):
471
472
  try:
472
473
  if FormatterRegistry.is_format_supported(format_type):
473
474
  # Use new FormatterRegistry
474
- formatter = FormatterRegistry.get_formatter(format_type)
475
- table_output = formatter.format(structure_result.elements)
475
+ registry_formatter = FormatterRegistry.get_formatter(
476
+ format_type
477
+ )
478
+ table_output = registry_formatter.format(
479
+ structure_result.elements
480
+ )
476
481
  else:
477
482
  # Fallback to legacy TableFormatter for backward compatibility
478
- formatter: Any = TableFormatter(format_type)
479
- table_output = formatter.format_structure(structure_dict) # type: ignore[attr-defined]
483
+ legacy_formatter: Any = TableFormatter(format_type)
484
+ table_output = legacy_formatter.format_structure(structure_dict)
480
485
  except Exception as e:
481
486
  # If FormatterRegistry fails, fallback to legacy TableFormatter
482
487
  logger.warning(
483
488
  f"FormatterRegistry failed, using legacy formatter: {e}"
484
489
  )
485
- formatter: Any = TableFormatter(format_type)
486
- table_output = formatter.format_structure(structure_dict) # type: ignore[attr-defined]
490
+ fallback_formatter: Any = TableFormatter(format_type)
491
+ table_output = fallback_formatter.format_structure(structure_dict)
487
492
 
488
493
  # Ensure output format matches CLI exactly
489
494
  # Fix line ending differences: normalize to Unix-style LF (\n)
@@ -36,7 +36,7 @@ class FileOutputManager:
36
36
  project_root: Optional project root directory for fallback output path
37
37
  """
38
38
  self.project_root = project_root
39
- self._output_path = None
39
+ self._output_path: str | None = None
40
40
  self._initialize_output_path()
41
41
 
42
42
  @classmethod
@@ -252,6 +252,10 @@ class FileOutputManager:
252
252
  output_file = output_path / filename
253
253
  else:
254
254
  # Generate filename with appropriate extension
255
+ if base_name is None:
256
+ raise ValueError(
257
+ "base_name cannot be None when filename is not provided"
258
+ )
255
259
  generated_filename = self.generate_output_filename(base_name, content)
256
260
  output_file = output_path / generated_filename
257
261
 
@@ -260,8 +264,9 @@ class FileOutputManager:
260
264
 
261
265
  # Write content to file
262
266
  try:
263
- with open(output_file, "w", encoding="utf-8") as f:
264
- f.write(content)
267
+ from ...encoding_utils import write_file_safe
268
+
269
+ write_file_safe(output_file, content)
265
270
 
266
271
  logger.info(f"Content saved to file: {output_file}")
267
272
  return str(output_file)
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
16
16
  class GitignoreDetector:
17
17
  """Detects .gitignore interference with file searches"""
18
18
 
19
- def __init__(self):
19
+ def __init__(self) -> None:
20
20
  self.common_ignore_patterns = {
21
21
  # Directory patterns that commonly cause search issues
22
22
  "build/*",
@@ -116,8 +116,10 @@ class GitignoreDetector:
116
116
  current_search_dir: Directory where the search is being performed
117
117
  """
118
118
  try:
119
- with open(gitignore_file, encoding="utf-8", errors="ignore") as f:
120
- lines = f.readlines()
119
+ from ...encoding_utils import read_file_safe
120
+
121
+ content, _ = read_file_safe(gitignore_file)
122
+ lines = content.splitlines()
121
123
 
122
124
  for line in lines:
123
125
  line = line.strip()
@@ -257,14 +259,14 @@ class GitignoreDetector:
257
259
 
258
260
  def get_detection_info(
259
261
  self, roots: list[str], project_root: str | None = None
260
- ) -> dict:
262
+ ) -> dict[str, object]:
261
263
  """
262
264
  Get detailed information about gitignore detection
263
265
 
264
266
  Returns:
265
267
  Dictionary with detection details for debugging/logging
266
268
  """
267
- info = {
269
+ info: dict[str, object] = {
268
270
  "should_use_no_ignore": False,
269
271
  "detected_gitignore_files": [],
270
272
  "interfering_patterns": [],
@@ -294,13 +296,21 @@ class GitignoreDetector:
294
296
  gitignore_file, gitignore_dir, project_path
295
297
  )
296
298
  if patterns:
297
- info["interfering_patterns"].extend(patterns)
298
-
299
- if info["interfering_patterns"]:
299
+ existing_patterns = info.get("interfering_patterns", [])
300
+ if isinstance(existing_patterns, list):
301
+ info["interfering_patterns"] = existing_patterns + patterns
302
+ else:
303
+ info["interfering_patterns"] = patterns
304
+
305
+ interfering_patterns = info.get("interfering_patterns", [])
306
+ if interfering_patterns:
300
307
  info["should_use_no_ignore"] = True
301
- info["reason"] = (
302
- f"Found {len(info['interfering_patterns'])} interfering patterns"
308
+ pattern_count = (
309
+ len(interfering_patterns)
310
+ if isinstance(interfering_patterns, list)
311
+ else 0
303
312
  )
313
+ info["reason"] = f"Found {pattern_count} interfering patterns"
304
314
 
305
315
  except Exception as e:
306
316
  info["reason"] = f"Error during detection: {e}"
@@ -314,8 +324,10 @@ class GitignoreDetector:
314
324
  interfering = []
315
325
 
316
326
  try:
317
- with open(gitignore_file, encoding="utf-8", errors="ignore") as f:
318
- lines = f.readlines()
327
+ from ...encoding_utils import read_file_safe
328
+
329
+ content, _ = read_file_safe(gitignore_file)
330
+ lines = content.splitlines()
319
331
 
320
332
  for line in lines:
321
333
  line = line.strip()
@@ -52,7 +52,7 @@ def _normalize_path_cross_platform(path_str: str) -> str:
52
52
  from ctypes import wintypes
53
53
 
54
54
  # GetLongPathNameW function
55
- _GetLongPathNameW = ctypes.windll.kernel32.GetLongPathNameW # type: ignore[attr-defined]
55
+ _GetLongPathNameW = ctypes.windll.kernel32.GetLongPathNameW
56
56
  _GetLongPathNameW.argtypes = [
57
57
  wintypes.LPCWSTR,
58
58
  wintypes.LPWSTR,
@@ -110,7 +110,7 @@ class PathResolver:
110
110
  project_root: Optional project root directory for resolving relative paths
111
111
  """
112
112
  self.project_root = None
113
- self._cache = {} # Simple cache for resolved paths
113
+ self._cache: dict[str, str] = {} # Simple cache for resolved paths
114
114
  self._cache_size_limit = 100 # Limit cache size to prevent memory issues
115
115
 
116
116
  if project_root:
@@ -45,6 +45,14 @@ class CodeElement(ABC):
45
45
  raw_text: str = ""
46
46
  language: str = "unknown"
47
47
  docstring: str | None = None # JavaDoc/docstring for this element
48
+ element_type: str = "unknown"
49
+
50
+ def to_summary_item(self) -> dict[str, Any]:
51
+ return {
52
+ "name": self.name,
53
+ "type": self.element_type,
54
+ "lines": {"start": self.start_line, "end": self.end_line},
55
+ }
48
56
 
49
57
 
50
58
  @dataclass(frozen=False)
@@ -341,6 +349,14 @@ class JavaPackage:
341
349
  start_line: int = 0
342
350
  end_line: int = 0
343
351
 
352
+ def to_summary_item(self) -> dict[str, Any]:
353
+ """Return dictionary for summary item"""
354
+ return {
355
+ "name": self.name,
356
+ "type": "package",
357
+ "lines": {"start": self.start_line, "end": self.end_line},
358
+ }
359
+
344
360
 
345
361
  @dataclass(frozen=False)
346
362
  class AnalysisResult:
@@ -0,0 +1,2 @@
1
+ mcp\server.py: error: Source file found twice under different module names:
2
+ "tree_sitter_analyzer.mcp.server" and "mcp.server"
@@ -34,6 +34,10 @@ class ElementExtractor(ABC):
34
34
  meaningful code elements like functions, classes, variables, etc.
35
35
  """
36
36
 
37
+ def __init__(self) -> None:
38
+ """Initialize the element extractor."""
39
+ self.current_file: str = "" # Current file being processed
40
+
37
41
  @abstractmethod
38
42
  def extract_functions(
39
43
  self, tree: "tree_sitter.Tree", source_code: str
@@ -98,6 +102,36 @@ class ElementExtractor(ABC):
98
102
  """
99
103
  pass
100
104
 
105
+ def extract_packages(self, tree: "tree_sitter.Tree", source_code: str) -> list[Any]:
106
+ """
107
+ Extract package declarations from the syntax tree.
108
+
109
+ Args:
110
+ tree: Tree-sitter AST
111
+ source_code: Original source code
112
+
113
+ Returns:
114
+ List of extracted package objects
115
+ """
116
+ # Default implementation returns empty list
117
+ return []
118
+
119
+ def extract_annotations(
120
+ self, tree: "tree_sitter.Tree", source_code: str
121
+ ) -> list[Any]:
122
+ """
123
+ Extract annotations from the syntax tree.
124
+
125
+ Args:
126
+ tree: Tree-sitter AST
127
+ source_code: Original source code
128
+
129
+ Returns:
130
+ List of extracted annotation objects
131
+ """
132
+ # Default implementation returns empty list
133
+ return []
134
+
101
135
  def extract_all_elements(
102
136
  self, tree: "tree_sitter.Tree", source_code: str
103
137
  ) -> list[CodeElement]:
@@ -123,6 +157,38 @@ class ElementExtractor(ABC):
123
157
 
124
158
  return elements
125
159
 
160
+ def extract_html_elements(
161
+ self, tree: "tree_sitter.Tree", source_code: str
162
+ ) -> list[Any]:
163
+ """
164
+ Extract HTML elements from the syntax tree.
165
+
166
+ Args:
167
+ tree: Tree-sitter AST
168
+ source_code: Original source code
169
+
170
+ Returns:
171
+ List of extracted HTML elements
172
+ """
173
+ # Default implementation returns empty list
174
+ return []
175
+
176
+ def extract_css_rules(
177
+ self, tree: "tree_sitter.Tree", source_code: str
178
+ ) -> list[Any]:
179
+ """
180
+ Extract CSS rules from the syntax tree.
181
+
182
+ Args:
183
+ tree: Tree-sitter AST
184
+ source_code: Original source code
185
+
186
+ Returns:
187
+ List of extracted CSS rules
188
+ """
189
+ # Default implementation returns empty list
190
+ return []
191
+
126
192
 
127
193
  class LanguagePlugin(ABC):
128
194
  """
@@ -107,10 +107,16 @@ JAVA_QUERIES: dict[str, str] = {
107
107
  name: (identifier) @name
108
108
  body: (block) @body) @method_with_body
109
109
  """,
110
+ # Fixed: Match methods WITH annotations (at least one required)
111
+ # Uses alternation [(annotation) (marker_annotation)] to match both types:
112
+ # - marker_annotation: Annotations without parameters (e.g., @Override)
113
+ # - annotation: Annotations with parameters (e.g., @SuppressWarnings("unchecked"))
114
+ # The + quantifier requires at least one annotation
110
115
  "method_with_annotations": """
111
116
  (method_declaration
112
- (modifiers (annotation) @annotation)*
113
- name: (identifier) @name) @method_with_annotations
117
+ (modifiers
118
+ [(annotation) (marker_annotation)]+ @annotation)
119
+ name: (identifier) @name) @method
114
120
  """,
115
121
  # --- Inheritance Relations ---
116
122
  "extends_clause": """
@@ -181,7 +187,7 @@ JAVA_QUERIES: dict[str, str] = {
181
187
  # --- Structural Information Extraction Queries ---
182
188
  "javadoc_comment": """
183
189
  (block_comment) @javadoc_comment
184
- (#match? @javadoc_comment "^/\\*\\*")
190
+ (#match? @javadoc_comment "^/\\\\\\\\\\\\*\\\\\\\\\\\\*")
185
191
  """,
186
192
  "class_with_javadoc": """
187
193
  (class_declaration
@@ -56,13 +56,13 @@ JAVASCRIPT_QUERIES: dict[str, str] = {
56
56
  "class_declaration": """
57
57
  (class_declaration
58
58
  name: (identifier) @class_name
59
- superclass: (class_heritage)? @superclass
59
+ (class_heritage)? @superclass
60
60
  body: (class_body) @body) @class_declaration
61
61
  """,
62
62
  "class_expression": """
63
63
  (class_expression
64
64
  name: (identifier)? @class_name
65
- superclass: (class_heritage)? @superclass
65
+ (class_heritage)? @superclass
66
66
  body: (class_body) @body) @class_expression
67
67
  """,
68
68
  "class_method": """
@@ -542,13 +542,8 @@ FUNCTIONS = """
542
542
  CLASSES = """
543
543
  (class_declaration
544
544
  name: (identifier) @class.name
545
- superclass: (class_heritage)? @class.superclass
545
+ (class_heritage)? @class.superclass
546
546
  body: (class_body) @class.body) @class.declaration
547
-
548
- (class_expression
549
- name: (identifier)? @class.name
550
- superclass: (class_heritage)? @class.superclass
551
- body: (class_body) @class.body) @class.expression
552
547
  """
553
548
 
554
549
  VARIABLES = """
@@ -176,7 +176,7 @@ def get_available_queries() -> list[str]:
176
176
  return sorted(queries + aliases)
177
177
 
178
178
 
179
- def get_query_info(query_name: str) -> dict[str, str]:
179
+ def get_query_info(query_name: str) -> dict[str, str | bool]:
180
180
  """
181
181
  Get information about a query
182
182
 
@@ -40,7 +40,7 @@ IMPORTS = """
40
40
 
41
41
  (import_from_statement
42
42
  module_name: (dotted_name)? @import.module
43
- name: (import_list) @import.list) @import.from_list
43
+ name: (aliased_import) @import.aliased_item) @import.from_aliased
44
44
 
45
45
  (aliased_import
46
46
  name: (dotted_name) @import.name
@@ -335,7 +335,7 @@ PYTHON_QUERIES: dict[str, str] = {
335
335
  "import_from_list": """
336
336
  (import_from_statement
337
337
  module_name: (dotted_name)? @module_name
338
- name: (import_list) @import_list) @import_from_list
338
+ name: (aliased_import) @import_item) @import_from_list
339
339
  """,
340
340
  "aliased_import": """
341
341
  (aliased_import
@@ -45,8 +45,6 @@ class ProjectBoundaryManager:
45
45
  # Handle both string and Path objects
46
46
  if isinstance(project_root, str):
47
47
  project_path = Path(project_root)
48
- elif isinstance(project_root, Path):
49
- project_path = project_root
50
48
  else:
51
49
  raise SecurityError(f"Invalid project root type: {type(project_root)}")
52
50
 
@@ -150,18 +148,17 @@ class ProjectBoundaryManager:
150
148
  real_path = Path(file_path).resolve()
151
149
  try:
152
150
  rel_path = real_path.relative_to(Path(self.project_root))
153
- rel_path = str(rel_path)
154
151
  except ValueError:
155
152
  # Path is not relative to project root
156
153
  log_warning(f"Path not relative to project root: {file_path}")
157
154
  return None
158
155
 
159
156
  # Ensure relative path doesn't start with ..
160
- if rel_path.startswith(".."):
157
+ if str(rel_path).startswith(".."):
161
158
  log_warning(f"Relative path calculation failed: {rel_path}")
162
159
  return None
163
160
 
164
- return rel_path
161
+ return str(rel_path)
165
162
 
166
163
  except Exception as e:
167
164
  log_warning(f"Relative path calculation error: {e}")
@@ -219,9 +219,9 @@ class RegexSafetyChecker:
219
219
 
220
220
  # Calculate complexity score
221
221
  metrics["complexity_score"] = (
222
- metrics["length"] * 0.1
222
+ int(metrics["length"] * 0.1)
223
223
  + metrics["quantifiers"] * 2
224
- + metrics["groups"] * 1.5
224
+ + int(metrics["groups"] * 1.5)
225
225
  + metrics["alternations"] * 3
226
226
  + metrics["character_classes"] * 1
227
227
  )
@@ -36,6 +36,8 @@ class SecurityValidator:
36
36
  Args:
37
37
  project_root: Optional project root directory for boundary checks
38
38
  """
39
+ self.boundary_manager: ProjectBoundaryManager | None
40
+
39
41
  # Ensure project_root is properly resolved if provided
40
42
  if project_root:
41
43
  try:
@@ -488,7 +490,9 @@ class SecurityValidator:
488
490
  or "CI" in os.environ
489
491
  or "GITHUB_ACTIONS" in os.environ
490
492
  or any(
491
- "test" in arg.lower() for arg in os.sys.argv if hasattr(os, "sys")
493
+ "test" in arg.lower()
494
+ for arg in getattr(getattr(os, "sys", None), "argv", [])
495
+ if hasattr(os, "sys")
492
496
  )
493
497
  )
494
498
 
@@ -665,7 +665,7 @@ class TableFormatter:
665
665
 
666
666
  # Map<String,Object> -> M<S,O>
667
667
  if "Map<" in type_name:
668
- return (
668
+ return str(
669
669
  type_name.replace("Map<", "M<")
670
670
  .replace("String", "S")
671
671
  .replace("Object", "O")
@@ -673,17 +673,17 @@ class TableFormatter:
673
673
 
674
674
  # List<String> -> L<S>
675
675
  if "List<" in type_name:
676
- return type_name.replace("List<", "L<").replace("String", "S")
676
+ return str(type_name.replace("List<", "L<").replace("String", "S"))
677
677
 
678
678
  # String[] -> S[]
679
679
  if "[]" in type_name:
680
680
  base_type = type_name.replace("[]", "")
681
681
  if base_type:
682
- return type_mapping.get(base_type, base_type[0].upper()) + "[]"
682
+ return str(type_mapping.get(base_type, base_type[0].upper())) + "[]"
683
683
  else:
684
684
  return "O[]"
685
685
 
686
- return type_mapping.get(type_name, type_name)
686
+ return str(type_mapping.get(type_name, type_name))
687
687
 
688
688
  def _convert_visibility(self, visibility: str) -> str:
689
689
  """Convert visibility to symbol"""
@@ -3,132 +3,38 @@
3
3
  Utilities package for tree_sitter_analyzer.
4
4
 
5
5
  This package contains utility modules for various functionality
6
- including tree-sitter API compatibility.
6
+ including tree-sitter API compatibility and logging.
7
7
  """
8
8
 
9
9
  # Import from tree-sitter compatibility module
10
10
  from .tree_sitter_compat import TreeSitterQueryCompat, get_node_text_safe, log_api_info
11
11
 
12
-
13
- # Re-export logging functions from the parent utils module
14
- # We need to import these dynamically to avoid circular imports
15
- def _import_logging_functions():
16
- """Dynamically import logging functions to avoid circular imports."""
17
- import importlib.util
18
- import os
19
-
20
- # Import the utils.py file from the parent directory
21
- parent_dir = os.path.dirname(os.path.dirname(__file__))
22
- utils_path = os.path.join(parent_dir, "utils.py")
23
- spec = importlib.util.spec_from_file_location(
24
- "tree_sitter_analyzer_utils", utils_path
25
- )
26
- utils_module = importlib.util.module_from_spec(spec)
27
- spec.loader.exec_module(utils_module)
28
-
29
- return (
30
- utils_module.setup_logger,
31
- utils_module.log_debug,
32
- utils_module.log_error,
33
- utils_module.log_warning,
34
- utils_module.log_info,
35
- utils_module.log_performance,
36
- utils_module.QuietMode,
37
- utils_module.safe_print,
38
- utils_module.LoggingContext,
39
- utils_module.setup_performance_logger,
40
- utils_module.create_performance_logger,
41
- )
42
-
43
-
44
- # Import logging functions
45
- try:
46
- (
47
- setup_logger,
48
- log_debug,
49
- log_error,
50
- log_warning,
51
- log_info,
52
- log_performance,
53
- QuietMode,
54
- safe_print,
55
- LoggingContext,
56
- setup_performance_logger,
57
- create_performance_logger,
58
- ) = _import_logging_functions()
59
- except Exception:
60
- # Fallback logging functions if import fails
61
- def setup_logger(name="tree_sitter_analyzer", level=30):
62
- import logging
63
-
64
- logger = logging.getLogger(name)
65
- if not logger.handlers:
66
- handler = logging.StreamHandler()
67
- formatter = logging.Formatter(
68
- "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
69
- )
70
- handler.setFormatter(formatter)
71
- logger.addHandler(handler)
72
- logger.setLevel(level)
73
- return logger
74
-
75
- def log_debug(msg, *args, **kwargs):
76
- pass
77
-
78
- def log_error(msg, *args, **kwargs):
79
- print(f"ERROR: {msg}", *args)
80
-
81
- def log_warning(msg, *args, **kwargs):
82
- print(f"WARNING: {msg}", *args)
83
-
84
- def log_info(msg, *args, **kwargs):
85
- print(f"INFO: {msg}", *args)
86
-
87
- def log_performance(operation, execution_time=None, details=None):
88
- pass
89
-
90
- # Fallback QuietMode class
91
- class QuietMode:
92
- def __init__(self, enabled=True):
93
- self.enabled = enabled
94
-
95
- def __enter__(self):
96
- return self
97
-
98
- def __exit__(self, exc_type, exc_val, exc_tb):
99
- pass
100
-
101
- # Fallback LoggingContext class
102
- class LoggingContext:
103
- def __init__(self, enabled=True, level=None):
104
- self.enabled = enabled
105
- self.level = level
106
-
107
- def __enter__(self):
108
- return self
109
-
110
- def __exit__(self, exc_type, exc_val, exc_tb):
111
- pass
112
-
113
- def setup_performance_logger():
114
- import logging
115
-
116
- return logging.getLogger("performance")
117
-
118
- def create_performance_logger(name):
119
- import logging
120
-
121
- return logging.getLogger(f"{name}.performance")
122
-
123
- def safe_print(message, level="info", quiet=False):
124
- if not quiet:
125
- print(message)
126
-
12
+ # Import logging functions directly from logging module
13
+ from .logging import (
14
+ LoggingContext,
15
+ QuietMode,
16
+ SafeStreamHandler,
17
+ create_performance_logger,
18
+ log_debug,
19
+ log_error,
20
+ log_info,
21
+ log_performance,
22
+ log_warning,
23
+ logger,
24
+ perf_logger,
25
+ safe_print,
26
+ setup_logger,
27
+ setup_performance_logger,
28
+ setup_safe_logging_shutdown,
29
+ suppress_output,
30
+ )
127
31
 
128
32
  __all__ = [
33
+ # Tree-sitter compatibility
129
34
  "TreeSitterQueryCompat",
130
35
  "get_node_text_safe",
131
36
  "log_api_info",
37
+ # Logging functionality
132
38
  "setup_logger",
133
39
  "log_debug",
134
40
  "log_error",
@@ -140,4 +46,9 @@ __all__ = [
140
46
  "LoggingContext",
141
47
  "setup_performance_logger",
142
48
  "create_performance_logger",
49
+ "SafeStreamHandler",
50
+ "setup_safe_logging_shutdown",
51
+ "suppress_output",
52
+ "logger",
53
+ "perf_logger",
143
54
  ]
@@ -154,7 +154,7 @@ class SafeStreamHandler(logging.StreamHandler):
154
154
  A StreamHandler that safely handles closed streams
155
155
  """
156
156
 
157
- def __init__(self, stream=None):
157
+ def __init__(self, stream: Any = None) -> None:
158
158
  # Default to sys.stderr to keep stdout clean for MCP stdio
159
159
  super().__init__(stream if stream is not None else sys.stderr)
160
160
 
@@ -449,4 +449,4 @@ class LoggingContext:
449
449
  def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
450
450
  if self.enabled and self.old_level is not None:
451
451
  # Always restore the saved level
452
- self.target_logger.setLevel(self.old_level)
452
+ self.target_logger.setLevel(self.old_level)
@@ -235,7 +235,7 @@ def get_node_text_safe(node: Any, source_code: str, encoding: str = "utf-8") ->
235
235
  line = lines[start_point[0]]
236
236
  start_col = max(0, min(start_point[1], len(line)))
237
237
  end_col = max(start_col, min(end_point[1], len(line)))
238
- return line[start_col:end_col]
238
+ return str(line[start_col:end_col])
239
239
  else:
240
240
  # Multiple lines
241
241
  result_lines = []
@@ -258,7 +258,7 @@ def get_node_text_safe(node: Any, source_code: str, encoding: str = "utf-8") ->
258
258
  return ""
259
259
 
260
260
 
261
- def log_api_info():
261
+ def log_api_info() -> None:
262
262
  """Log information about available tree-sitter APIs."""
263
263
  try:
264
264
  import tree_sitter