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.
- tree_sitter_analyzer/__init__.py +1 -1
- tree_sitter_analyzer/api.py +23 -30
- tree_sitter_analyzer/cli/argument_validator.py +77 -0
- tree_sitter_analyzer/cli/commands/table_command.py +7 -2
- tree_sitter_analyzer/cli_main.py +17 -3
- tree_sitter_analyzer/core/cache_service.py +15 -5
- tree_sitter_analyzer/core/query.py +33 -22
- tree_sitter_analyzer/core/query_service.py +179 -154
- tree_sitter_analyzer/formatters/formatter_registry.py +355 -0
- tree_sitter_analyzer/formatters/html_formatter.py +462 -0
- tree_sitter_analyzer/formatters/language_formatter_factory.py +3 -0
- tree_sitter_analyzer/formatters/markdown_formatter.py +1 -1
- tree_sitter_analyzer/language_detector.py +80 -7
- tree_sitter_analyzer/languages/css_plugin.py +390 -0
- tree_sitter_analyzer/languages/html_plugin.py +395 -0
- tree_sitter_analyzer/languages/java_plugin.py +116 -0
- tree_sitter_analyzer/languages/javascript_plugin.py +113 -0
- tree_sitter_analyzer/languages/markdown_plugin.py +266 -46
- tree_sitter_analyzer/languages/python_plugin.py +176 -33
- tree_sitter_analyzer/languages/typescript_plugin.py +130 -1
- tree_sitter_analyzer/mcp/tools/query_tool.py +99 -58
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +24 -10
- tree_sitter_analyzer/models.py +53 -0
- tree_sitter_analyzer/output_manager.py +1 -1
- tree_sitter_analyzer/plugins/base.py +50 -0
- tree_sitter_analyzer/plugins/manager.py +5 -1
- tree_sitter_analyzer/queries/css.py +634 -0
- tree_sitter_analyzer/queries/html.py +556 -0
- tree_sitter_analyzer/queries/markdown.py +54 -164
- tree_sitter_analyzer/query_loader.py +16 -3
- tree_sitter_analyzer/security/validator.py +182 -44
- tree_sitter_analyzer/utils/__init__.py +113 -0
- tree_sitter_analyzer/utils/tree_sitter_compat.py +282 -0
- tree_sitter_analyzer/utils.py +62 -24
- {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/METADATA +120 -14
- {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/RECORD +38 -29
- {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/entry_points.txt +2 -0
- {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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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[
|
|
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[
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
458
|
-
|
|
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)
|
tree_sitter_analyzer/models.py
CHANGED
|
@@ -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
|
# ========================================
|
|
@@ -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
|
-
|
|
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]:
|