tree-sitter-analyzer 1.7.7__py3-none-any.whl → 1.8.2__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (38) hide show
  1. tree_sitter_analyzer/__init__.py +1 -1
  2. tree_sitter_analyzer/api.py +23 -30
  3. tree_sitter_analyzer/cli/argument_validator.py +77 -0
  4. tree_sitter_analyzer/cli/commands/table_command.py +7 -2
  5. tree_sitter_analyzer/cli_main.py +17 -3
  6. tree_sitter_analyzer/core/cache_service.py +15 -5
  7. tree_sitter_analyzer/core/query.py +33 -22
  8. tree_sitter_analyzer/core/query_service.py +179 -154
  9. tree_sitter_analyzer/formatters/formatter_registry.py +355 -0
  10. tree_sitter_analyzer/formatters/html_formatter.py +462 -0
  11. tree_sitter_analyzer/formatters/language_formatter_factory.py +3 -0
  12. tree_sitter_analyzer/formatters/markdown_formatter.py +1 -1
  13. tree_sitter_analyzer/language_detector.py +80 -7
  14. tree_sitter_analyzer/languages/css_plugin.py +390 -0
  15. tree_sitter_analyzer/languages/html_plugin.py +395 -0
  16. tree_sitter_analyzer/languages/java_plugin.py +116 -0
  17. tree_sitter_analyzer/languages/javascript_plugin.py +113 -0
  18. tree_sitter_analyzer/languages/markdown_plugin.py +266 -46
  19. tree_sitter_analyzer/languages/python_plugin.py +176 -33
  20. tree_sitter_analyzer/languages/typescript_plugin.py +130 -1
  21. tree_sitter_analyzer/mcp/tools/query_tool.py +99 -58
  22. tree_sitter_analyzer/mcp/tools/table_format_tool.py +24 -10
  23. tree_sitter_analyzer/models.py +53 -0
  24. tree_sitter_analyzer/output_manager.py +1 -1
  25. tree_sitter_analyzer/plugins/base.py +50 -0
  26. tree_sitter_analyzer/plugins/manager.py +5 -1
  27. tree_sitter_analyzer/queries/css.py +634 -0
  28. tree_sitter_analyzer/queries/html.py +556 -0
  29. tree_sitter_analyzer/queries/markdown.py +54 -164
  30. tree_sitter_analyzer/query_loader.py +16 -3
  31. tree_sitter_analyzer/security/validator.py +182 -44
  32. tree_sitter_analyzer/utils/__init__.py +113 -0
  33. tree_sitter_analyzer/utils/tree_sitter_compat.py +282 -0
  34. tree_sitter_analyzer/utils.py +62 -24
  35. {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/METADATA +120 -14
  36. {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/RECORD +38 -29
  37. {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/entry_points.txt +2 -0
  38. {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/WHEEL +0 -0
@@ -1726,4 +1726,133 @@ class TypeScriptPlugin(LanguagePlugin):
1726
1726
  all_elements.extend(extractor.extract_variables(tree, source_code))
1727
1727
  all_elements.extend(extractor.extract_imports(tree, source_code))
1728
1728
 
1729
- return all_elements
1729
+ return all_elements
1730
+
1731
+ def execute_query_strategy(self, tree: "tree_sitter.Tree", source_code: str, query_key: str) -> list[CodeElement]:
1732
+ """Execute TypeScript-specific query strategy based on query_key"""
1733
+ if not tree or not source_code:
1734
+ return []
1735
+
1736
+ # Initialize extractor with source code
1737
+ self._extractor.source_code = source_code
1738
+ self._extractor.content_lines = source_code.split("\n")
1739
+ self._extractor._reset_caches()
1740
+ self._extractor._detect_file_characteristics()
1741
+
1742
+ # Map query_key to appropriate extraction method
1743
+ query_mapping = {
1744
+ # Function-related queries
1745
+ "function": lambda: self._extractor.extract_functions(tree, source_code),
1746
+ "async_function": lambda: [f for f in self._extractor.extract_functions(tree, source_code) if getattr(f, 'is_async', False)],
1747
+ "arrow_function": lambda: [f for f in self._extractor.extract_functions(tree, source_code) if getattr(f, 'is_arrow', False)],
1748
+ "method": lambda: [f for f in self._extractor.extract_functions(tree, source_code) if getattr(f, 'is_method', False)],
1749
+ "constructor": lambda: [f for f in self._extractor.extract_functions(tree, source_code) if getattr(f, 'is_constructor', False)],
1750
+ "signature": lambda: [f for f in self._extractor.extract_functions(tree, source_code) if getattr(f, 'is_signature', False)],
1751
+
1752
+ # Class-related queries
1753
+ "class": lambda: self._extractor.extract_classes(tree, source_code),
1754
+ "interface": lambda: [c for c in self._extractor.extract_classes(tree, source_code) if getattr(c, 'class_type', '') == 'interface'],
1755
+ "type_alias": lambda: [c for c in self._extractor.extract_classes(tree, source_code) if getattr(c, 'class_type', '') == 'type'],
1756
+ "enum": lambda: [c for c in self._extractor.extract_classes(tree, source_code) if getattr(c, 'class_type', '') == 'enum'],
1757
+
1758
+ # Variable-related queries
1759
+ "variable": lambda: self._extractor.extract_variables(tree, source_code),
1760
+
1761
+ # Import/Export queries
1762
+ "import": lambda: self._extractor.extract_imports(tree, source_code),
1763
+ "export": lambda: [i for i in self._extractor.extract_imports(tree, source_code) if 'export' in getattr(i, 'raw_text', '')],
1764
+
1765
+ # TypeScript-specific queries
1766
+ "generic": lambda: [c for c in self._extractor.extract_classes(tree, source_code) if 'generics' in getattr(c, 'raw_text', '')],
1767
+ "decorator": lambda: [f for f in self._extractor.extract_functions(tree, source_code) if '@' in getattr(f, 'raw_text', '')],
1768
+
1769
+ # Framework-specific queries
1770
+ "react_component": lambda: [c for c in self._extractor.extract_classes(tree, source_code) if getattr(c, 'is_react_component', False)],
1771
+ "angular_component": lambda: [c for c in self._extractor.extract_classes(tree, source_code) if getattr(c, 'framework_type', '') == 'angular'],
1772
+ "vue_component": lambda: [c for c in self._extractor.extract_classes(tree, source_code) if getattr(c, 'framework_type', '') == 'vue'],
1773
+ }
1774
+
1775
+ # Execute the appropriate extraction method
1776
+ if query_key in query_mapping:
1777
+ try:
1778
+ return query_mapping[query_key]()
1779
+ except Exception as e:
1780
+ log_error(f"Error executing TypeScript query '{query_key}': {e}")
1781
+ return []
1782
+ else:
1783
+ log_warning(f"Unsupported TypeScript query key: {query_key}")
1784
+ return []
1785
+
1786
+ def get_element_categories(self) -> dict[str, list[str]]:
1787
+ """Get TypeScript element categories mapping query_key to node_types"""
1788
+ return {
1789
+ # Function-related categories
1790
+ "function": [
1791
+ "function_declaration",
1792
+ "function_expression",
1793
+ "arrow_function",
1794
+ "generator_function_declaration"
1795
+ ],
1796
+ "async_function": [
1797
+ "function_declaration",
1798
+ "function_expression",
1799
+ "arrow_function",
1800
+ "method_definition"
1801
+ ],
1802
+ "arrow_function": ["arrow_function"],
1803
+ "method": [
1804
+ "method_definition",
1805
+ "method_signature"
1806
+ ],
1807
+ "constructor": ["method_definition"],
1808
+ "signature": ["method_signature"],
1809
+
1810
+ # Class-related categories
1811
+ "class": [
1812
+ "class_declaration",
1813
+ "abstract_class_declaration"
1814
+ ],
1815
+ "interface": ["interface_declaration"],
1816
+ "type_alias": ["type_alias_declaration"],
1817
+ "enum": ["enum_declaration"],
1818
+
1819
+ # Variable-related categories
1820
+ "variable": [
1821
+ "variable_declaration",
1822
+ "lexical_declaration",
1823
+ "property_definition",
1824
+ "property_signature"
1825
+ ],
1826
+
1827
+ # Import/Export categories
1828
+ "import": ["import_statement"],
1829
+ "export": [
1830
+ "export_statement",
1831
+ "export_declaration"
1832
+ ],
1833
+
1834
+ # TypeScript-specific categories
1835
+ "generic": [
1836
+ "type_parameters",
1837
+ "type_parameter"
1838
+ ],
1839
+ "decorator": [
1840
+ "decorator",
1841
+ "decorator_call_expression"
1842
+ ],
1843
+
1844
+ # Framework-specific categories
1845
+ "react_component": [
1846
+ "class_declaration",
1847
+ "function_declaration",
1848
+ "arrow_function"
1849
+ ],
1850
+ "angular_component": [
1851
+ "class_declaration",
1852
+ "decorator"
1853
+ ],
1854
+ "vue_component": [
1855
+ "class_declaration",
1856
+ "function_declaration"
1857
+ ]
1858
+ }
@@ -97,7 +97,6 @@ class QueryTool(BaseMCPTool):
97
97
  },
98
98
  }
99
99
 
100
- @handle_mcp_errors("query_code")
101
100
  async def execute(self, arguments: dict[str, Any]) -> dict[str, Any]:
102
101
  """
103
102
  Execute query tool
@@ -108,53 +107,78 @@ class QueryTool(BaseMCPTool):
108
107
  Returns:
109
108
  Query results
110
109
  """
111
- # Validate input parameters
112
- file_path = arguments.get("file_path")
113
- if not file_path:
114
- raise ValueError("file_path is required")
110
+ try:
111
+ # Validate input parameters - check for empty arguments first
112
+ if not arguments:
113
+ from ..utils.error_handler import AnalysisError
114
+ raise AnalysisError(
115
+ "file_path is required",
116
+ operation="query_code"
117
+ )
118
+
119
+ file_path = arguments.get("file_path")
120
+ if not file_path:
121
+ from ..utils.error_handler import AnalysisError
122
+ raise AnalysisError(
123
+ "file_path is required",
124
+ operation="query_code"
125
+ )
126
+
127
+ # Check that either query_key or query_string is provided early
128
+ query_key = arguments.get("query_key")
129
+ query_string = arguments.get("query_string")
130
+
131
+ if not query_key and not query_string:
132
+ from ..utils.error_handler import AnalysisError
133
+ raise AnalysisError(
134
+ "Either query_key or query_string must be provided",
135
+ operation="query_code"
136
+ )
137
+
138
+ # Security validation BEFORE path resolution to catch symlinks
139
+ is_valid, error_msg = self.security_validator.validate_file_path(file_path)
140
+ if not is_valid:
141
+ return {
142
+ "success": False,
143
+ "error": f"Invalid or unsafe file path: {error_msg or file_path}"
144
+ }
115
145
 
116
- # Security validation BEFORE path resolution to catch symlinks
117
- is_valid, error_msg = self.security_validator.validate_file_path(file_path)
118
- if not is_valid:
119
- raise ValueError(
120
- f"Invalid or unsafe file path: {error_msg or file_path}"
121
- )
146
+ # Resolve file path to absolute path
147
+ resolved_file_path = self.path_resolver.resolve(file_path)
148
+ logger.info(f"Querying file: {file_path} (resolved to: {resolved_file_path})")
122
149
 
123
- # Resolve file path to absolute path
124
- resolved_file_path = self.path_resolver.resolve(file_path)
125
- logger.info(f"Querying file: {file_path} (resolved to: {resolved_file_path})")
126
-
127
- # Additional security validation on resolved path
128
- is_valid, error_msg = self.security_validator.validate_file_path(
129
- resolved_file_path
130
- )
131
- if not is_valid:
132
- raise ValueError(
133
- f"Invalid or unsafe resolved path: {error_msg or resolved_file_path}"
150
+ # Additional security validation on resolved path
151
+ is_valid, error_msg = self.security_validator.validate_file_path(
152
+ resolved_file_path
134
153
  )
154
+ if not is_valid:
155
+ return {
156
+ "success": False,
157
+ "error": f"Invalid or unsafe resolved path: {error_msg or resolved_file_path}"
158
+ }
135
159
 
136
- # Get query parameters
137
- query_key = arguments.get("query_key")
138
- query_string = arguments.get("query_string")
139
- filter_expression = arguments.get("filter")
140
- output_format = arguments.get("output_format", "json")
141
- output_file = arguments.get("output_file")
142
- suppress_output = arguments.get("suppress_output", False)
143
-
144
- if not query_key and not query_string:
145
- raise ValueError("Either query_key or query_string must be provided")
160
+ # Get query parameters (already validated above)
161
+ filter_expression = arguments.get("filter")
162
+ output_format = arguments.get("output_format", "json")
163
+ output_file = arguments.get("output_file")
164
+ suppress_output = arguments.get("suppress_output", False)
146
165
 
147
- if query_key and query_string:
148
- raise ValueError("Cannot provide both query_key and query_string")
166
+ if query_key and query_string:
167
+ return {
168
+ "success": False,
169
+ "error": "Cannot provide both query_key and query_string"
170
+ }
149
171
 
150
- # Detect language
151
- language = arguments.get("language")
152
- if not language:
153
- language = detect_language_from_file(resolved_file_path)
172
+ # Detect language
173
+ language = arguments.get("language")
154
174
  if not language:
155
- raise ValueError(f"Could not detect language for file: {file_path}")
175
+ language = detect_language_from_file(resolved_file_path)
176
+ if not language:
177
+ return {
178
+ "success": False,
179
+ "error": f"Could not detect language for file: {file_path}"
180
+ }
156
181
 
157
- try:
158
182
  # Execute query
159
183
  results = await self.query_service.execute_query(
160
184
  resolved_file_path, language, query_key, query_string, filter_expression
@@ -170,7 +194,9 @@ class QueryTool(BaseMCPTool):
170
194
 
171
195
  # Format output
172
196
  if output_format == "summary":
173
- formatted_result = self._format_summary(results, query_key or "custom", language)
197
+ formatted_result = self._format_summary(
198
+ results, query_key or "custom", language
199
+ )
174
200
  else:
175
201
  formatted_result = {
176
202
  "success": True,
@@ -185,28 +211,31 @@ class QueryTool(BaseMCPTool):
185
211
  if output_file:
186
212
  try:
187
213
  import json
188
-
214
+
189
215
  # Generate base name from original file path if not provided
190
216
  if not output_file or output_file.strip() == "":
191
- base_name = f"{Path(file_path).stem}_query_{query_key or 'custom'}"
217
+ base_name = (
218
+ f"{Path(file_path).stem}_query_{query_key or 'custom'}"
219
+ )
192
220
  else:
193
221
  base_name = output_file
194
222
 
195
223
  # Convert result to JSON string for file output
196
- json_content = json.dumps(formatted_result, indent=2, ensure_ascii=False)
224
+ json_content = json.dumps(
225
+ formatted_result, indent=2, ensure_ascii=False
226
+ )
197
227
 
198
228
  # Save to file with automatic extension detection
199
229
  saved_file_path = self.file_output_manager.save_to_file(
200
- content=json_content,
201
- base_name=base_name
230
+ content=json_content, base_name=base_name
202
231
  )
203
-
232
+
204
233
  # Add file output info to result
205
234
  formatted_result["output_file_path"] = saved_file_path
206
235
  formatted_result["file_saved"] = True
207
-
236
+
208
237
  logger.info(f"Query output saved to: {saved_file_path}")
209
-
238
+
210
239
  except Exception as e:
211
240
  logger.error(f"Failed to save output to file: {e}")
212
241
  formatted_result["file_save_error"] = str(e)
@@ -222,26 +251,35 @@ class QueryTool(BaseMCPTool):
222
251
  "language": language,
223
252
  "query": query_key or query_string,
224
253
  }
225
-
254
+
226
255
  # Include file output info if present
227
256
  if "output_file_path" in formatted_result:
228
- minimal_result["output_file_path"] = formatted_result["output_file_path"]
257
+ minimal_result["output_file_path"] = formatted_result[
258
+ "output_file_path"
259
+ ]
229
260
  minimal_result["file_saved"] = formatted_result["file_saved"]
230
261
  if "file_save_error" in formatted_result:
231
- minimal_result["file_save_error"] = formatted_result["file_save_error"]
262
+ minimal_result["file_save_error"] = formatted_result[
263
+ "file_save_error"
264
+ ]
232
265
  minimal_result["file_saved"] = formatted_result["file_saved"]
233
-
266
+
234
267
  return minimal_result
235
268
  else:
236
269
  return formatted_result
237
270
 
238
271
  except Exception as e:
272
+ from ..utils.error_handler import AnalysisError
273
+ # Re-raise AnalysisError to maintain proper error handling
274
+ if isinstance(e, AnalysisError):
275
+ raise
276
+
239
277
  logger.error(f"Query execution failed: {e}")
240
278
  return {
241
279
  "success": False,
242
280
  "error": str(e),
243
- "file_path": file_path,
244
- "language": language,
281
+ "file_path": arguments.get("file_path", "unknown"),
282
+ "language": arguments.get("language", "unknown"),
245
283
  }
246
284
 
247
285
  def _format_summary(
@@ -259,7 +297,7 @@ class QueryTool(BaseMCPTool):
259
297
  Summary formatted results
260
298
  """
261
299
  # Group by capture name
262
- by_capture = {}
300
+ by_capture: dict[str, list[dict[str, Any]]] = {}
263
301
  for result in results:
264
302
  capture_name = result["capture_name"]
265
303
  if capture_name not in by_capture:
@@ -267,7 +305,7 @@ class QueryTool(BaseMCPTool):
267
305
  by_capture[capture_name].append(result)
268
306
 
269
307
  # Create summary
270
- summary = {
308
+ summary: dict[str, Any] = {
271
309
  "success": True,
272
310
  "query_type": query_type,
273
311
  "language": language,
@@ -309,6 +347,9 @@ class QueryTool(BaseMCPTool):
309
347
 
310
348
  # Match common declaration patterns
311
349
  patterns = [
350
+ # Markdown headers
351
+ r"^#{1,6}\s+(.+)$", # Markdown headers (# Title, ## Subtitle, etc.)
352
+ # Programming language patterns
312
353
  r"(?:public|private|protected)?\s*(?:static)?\s*(?:class|interface)\s+(\w+)", # class/interface
313
354
  r"(?:public|private|protected)?\s*(?:static)?\s*\w+\s+(\w+)\s*\(", # method
314
355
  r"(\w+)\s*\(", # simple function call
@@ -317,7 +358,7 @@ class QueryTool(BaseMCPTool):
317
358
  for pattern in patterns:
318
359
  match = re.search(pattern, first_line)
319
360
  if match:
320
- return match.group(1)
361
+ return match.group(1).strip()
321
362
 
322
363
  return "unnamed"
323
364
 
@@ -18,6 +18,7 @@ from ...constants import (
18
18
  is_element_of_type,
19
19
  )
20
20
  from ...core.analysis_engine import AnalysisRequest, get_analysis_engine
21
+ from ...formatters.formatter_registry import FormatterRegistry
21
22
  from ...language_detector import detect_language_from_file
22
23
  from ...table_formatter import TableFormatter
23
24
  from ...utils import setup_logger
@@ -73,7 +74,7 @@ class TableFormatTool(BaseMCPTool):
73
74
  "format_type": {
74
75
  "type": "string",
75
76
  "description": "Table format type",
76
- "enum": ["full", "compact", "csv", "json"],
77
+ "enum": list(set(FormatterRegistry.get_available_formats() + ["full", "compact", "csv", "json"])),
77
78
  "default": "full",
78
79
  },
79
80
  "language": {
@@ -123,8 +124,11 @@ class TableFormatTool(BaseMCPTool):
123
124
  format_type = arguments["format_type"]
124
125
  if not isinstance(format_type, str):
125
126
  raise ValueError("format_type must be a string")
126
- if format_type not in ["full", "compact", "csv", "json"]:
127
- raise ValueError("format_type must be one of: full, compact, csv, json")
127
+
128
+ # Check both new FormatterRegistry formats and legacy formats
129
+ available_formats = list(set(FormatterRegistry.get_available_formats() + ["full", "compact", "csv", "json"]))
130
+ if format_type not in available_formats:
131
+ raise ValueError(f"format_type must be one of: {', '.join(sorted(available_formats))}")
128
132
 
129
133
  # Validate language if provided
130
134
  if "language" in arguments:
@@ -448,14 +452,24 @@ class TableFormatTool(BaseMCPTool):
448
452
  f"Failed to analyze structure for file: {file_path}"
449
453
  )
450
454
 
451
- # Create table formatter
452
- formatter = TableFormatter(format_type)
453
-
454
- # Convert AnalysisResult to dict format for TableFormatter
455
+ # Always convert analysis result to dict for metadata extraction
455
456
  structure_dict = self._convert_analysis_result_to_dict(structure_result)
456
-
457
- # Format table
458
- table_output = formatter.format_structure(structure_dict)
457
+
458
+ # Try to use new FormatterRegistry first, fallback to legacy TableFormatter
459
+ try:
460
+ if FormatterRegistry.is_format_supported(format_type):
461
+ # Use new FormatterRegistry
462
+ formatter = FormatterRegistry.get_formatter(format_type)
463
+ table_output = formatter.format(structure_result.elements)
464
+ else:
465
+ # Fallback to legacy TableFormatter for backward compatibility
466
+ formatter = TableFormatter(format_type)
467
+ table_output = formatter.format_structure(structure_dict)
468
+ except Exception as e:
469
+ # If FormatterRegistry fails, fallback to legacy TableFormatter
470
+ logger.warning(f"FormatterRegistry failed, using legacy formatter: {e}")
471
+ formatter = TableFormatter(format_type)
472
+ table_output = formatter.format_structure(structure_dict)
459
473
 
460
474
  # Ensure output format matches CLI exactly
461
475
  # Fix line ending differences: normalize to Unix-style LF (\n)
@@ -150,6 +150,59 @@ class Package(CodeElement):
150
150
  element_type: str = "package"
151
151
 
152
152
 
153
+ # ========================================
154
+ # HTML/CSS-Specific Models
155
+ # ========================================
156
+
157
+
158
+ @dataclass(frozen=False)
159
+ class MarkupElement(CodeElement):
160
+ """
161
+ HTML要素を表現するデータモデル。
162
+ CodeElementを継承し、マークアップ固有の属性を追加する。
163
+ """
164
+
165
+ tag_name: str = ""
166
+ attributes: dict[str, str] = field(default_factory=dict)
167
+ parent: "MarkupElement | None" = None
168
+ children: list["MarkupElement"] = field(default_factory=list)
169
+ element_class: str = "" # 分類システムのカテゴリ (例: 'structure', 'media', 'form')
170
+ element_type: str = "html_element"
171
+
172
+ def to_summary_item(self) -> dict[str, Any]:
173
+ """Return dictionary for summary item"""
174
+ return {
175
+ "name": self.name,
176
+ "tag_name": self.tag_name,
177
+ "type": "html_element",
178
+ "element_class": self.element_class,
179
+ "lines": {"start": self.start_line, "end": self.end_line},
180
+ }
181
+
182
+
183
+ @dataclass(frozen=False)
184
+ class StyleElement(CodeElement):
185
+ """
186
+ CSSルールを表現するデータモデル。
187
+ CodeElementを継承する。
188
+ """
189
+
190
+ selector: str = ""
191
+ properties: dict[str, str] = field(default_factory=dict)
192
+ element_class: str = "" # 分類システムのカテゴリ (例: 'layout', 'typography', 'color')
193
+ element_type: str = "css_rule"
194
+
195
+ def to_summary_item(self) -> dict[str, Any]:
196
+ """Return dictionary for summary item"""
197
+ return {
198
+ "name": self.name,
199
+ "selector": self.selector,
200
+ "type": "css_rule",
201
+ "element_class": self.element_class,
202
+ "lines": {"start": self.start_line, "end": self.end_line},
203
+ }
204
+
205
+
153
206
  # ========================================
154
207
  # Java-Specific Models
155
208
  # ========================================
@@ -28,7 +28,7 @@ class OutputManager:
28
28
  """Output warning message"""
29
29
  if not self.quiet:
30
30
  print(f"WARNING: {message}", file=sys.stderr)
31
- log_warning(message)
31
+ log_warning(message)
32
32
 
33
33
  def error(self, message: str) -> None:
34
34
  """Output error message"""
@@ -178,6 +178,56 @@ class LanguagePlugin(ABC):
178
178
  """
179
179
  pass
180
180
 
181
+ def get_supported_element_types(self) -> list[str]:
182
+ """
183
+ Return list of supported CodeElement types.
184
+
185
+ Returns:
186
+ List of element types (e.g., ["function", "class", "variable"])
187
+ """
188
+ return ["function", "class", "variable", "import"]
189
+
190
+ def get_queries(self) -> dict[str, str]:
191
+ """
192
+ Return language-specific tree-sitter queries.
193
+
194
+ Returns:
195
+ Dictionary mapping query names to query strings
196
+ """
197
+ return {}
198
+
199
+ def execute_query_strategy(self, query_key: str | None, language: str) -> str | None:
200
+ """
201
+ Execute query strategy for this language plugin.
202
+
203
+ Args:
204
+ query_key: Query key to execute
205
+ language: Programming language
206
+
207
+ Returns:
208
+ Query string or None if not supported
209
+ """
210
+ queries = self.get_queries()
211
+ return queries.get(query_key) if query_key else None
212
+
213
+ def get_formatter_map(self) -> dict[str, str]:
214
+ """
215
+ Return mapping of format types to formatter class names.
216
+
217
+ Returns:
218
+ Dictionary mapping format names to formatter classes
219
+ """
220
+ return {}
221
+
222
+ def get_element_categories(self) -> dict[str, list[str]]:
223
+ """
224
+ Return element categories for HTML/CSS languages.
225
+
226
+ Returns:
227
+ Dictionary mapping category names to element lists
228
+ """
229
+ return {}
230
+
181
231
  def is_applicable(self, file_path: str) -> bool:
182
232
  """
183
233
  Check if this plugin is applicable for the given file.
@@ -64,7 +64,11 @@ class PluginManager:
64
64
  log_debug(f"Skipping duplicate plugin for language: {language}")
65
65
 
66
66
  final_plugins = list(unique_plugins.values())
67
- log_info(f"Successfully loaded {len(final_plugins)} plugins")
67
+ # Only log if not in CLI mode (check if we're in quiet mode)
68
+ import os
69
+ log_level = os.environ.get("LOG_LEVEL", "WARNING")
70
+ if log_level != "ERROR":
71
+ log_info(f"Successfully loaded {len(final_plugins)} plugins")
68
72
  return final_plugins
69
73
 
70
74
  def _load_from_entry_points(self) -> list[LanguagePlugin]: