tree-sitter-analyzer 1.9.17.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tree_sitter_analyzer/__init__.py +132 -0
- tree_sitter_analyzer/__main__.py +11 -0
- tree_sitter_analyzer/api.py +853 -0
- tree_sitter_analyzer/cli/__init__.py +39 -0
- tree_sitter_analyzer/cli/__main__.py +12 -0
- tree_sitter_analyzer/cli/argument_validator.py +89 -0
- tree_sitter_analyzer/cli/commands/__init__.py +26 -0
- tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
- tree_sitter_analyzer/cli/commands/base_command.py +181 -0
- tree_sitter_analyzer/cli/commands/default_command.py +18 -0
- tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
- tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
- tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
- tree_sitter_analyzer/cli/commands/query_command.py +109 -0
- tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
- tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
- tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
- tree_sitter_analyzer/cli/commands/table_command.py +414 -0
- tree_sitter_analyzer/cli/info_commands.py +124 -0
- tree_sitter_analyzer/cli_main.py +472 -0
- tree_sitter_analyzer/constants.py +85 -0
- tree_sitter_analyzer/core/__init__.py +15 -0
- tree_sitter_analyzer/core/analysis_engine.py +580 -0
- tree_sitter_analyzer/core/cache_service.py +333 -0
- tree_sitter_analyzer/core/engine.py +585 -0
- tree_sitter_analyzer/core/parser.py +293 -0
- tree_sitter_analyzer/core/query.py +605 -0
- tree_sitter_analyzer/core/query_filter.py +200 -0
- tree_sitter_analyzer/core/query_service.py +340 -0
- tree_sitter_analyzer/encoding_utils.py +530 -0
- tree_sitter_analyzer/exceptions.py +747 -0
- tree_sitter_analyzer/file_handler.py +246 -0
- tree_sitter_analyzer/formatters/__init__.py +1 -0
- tree_sitter_analyzer/formatters/base_formatter.py +201 -0
- tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
- tree_sitter_analyzer/formatters/formatter_config.py +197 -0
- tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
- tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
- tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
- tree_sitter_analyzer/formatters/go_formatter.py +368 -0
- tree_sitter_analyzer/formatters/html_formatter.py +498 -0
- tree_sitter_analyzer/formatters/java_formatter.py +423 -0
- tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
- tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
- tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
- tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
- tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
- tree_sitter_analyzer/formatters/php_formatter.py +301 -0
- tree_sitter_analyzer/formatters/python_formatter.py +830 -0
- tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
- tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
- tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
- tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
- tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
- tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
- tree_sitter_analyzer/interfaces/__init__.py +9 -0
- tree_sitter_analyzer/interfaces/cli.py +535 -0
- tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
- tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
- tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
- tree_sitter_analyzer/language_detector.py +553 -0
- tree_sitter_analyzer/language_loader.py +271 -0
- tree_sitter_analyzer/languages/__init__.py +10 -0
- tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
- tree_sitter_analyzer/languages/css_plugin.py +449 -0
- tree_sitter_analyzer/languages/go_plugin.py +836 -0
- tree_sitter_analyzer/languages/html_plugin.py +496 -0
- tree_sitter_analyzer/languages/java_plugin.py +1299 -0
- tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
- tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
- tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
- tree_sitter_analyzer/languages/php_plugin.py +862 -0
- tree_sitter_analyzer/languages/python_plugin.py +1636 -0
- tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
- tree_sitter_analyzer/languages/rust_plugin.py +673 -0
- tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
- tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
- tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
- tree_sitter_analyzer/legacy_table_formatter.py +860 -0
- tree_sitter_analyzer/mcp/__init__.py +34 -0
- tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
- tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
- tree_sitter_analyzer/mcp/server.py +869 -0
- tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
- tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
- tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
- tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
- tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
- tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
- tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
- tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
- tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
- tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
- tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
- tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
- tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
- tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
- tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
- tree_sitter_analyzer/models.py +840 -0
- tree_sitter_analyzer/mypy_current_errors.txt +2 -0
- tree_sitter_analyzer/output_manager.py +255 -0
- tree_sitter_analyzer/platform_compat/__init__.py +3 -0
- tree_sitter_analyzer/platform_compat/adapter.py +324 -0
- tree_sitter_analyzer/platform_compat/compare.py +224 -0
- tree_sitter_analyzer/platform_compat/detector.py +67 -0
- tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
- tree_sitter_analyzer/platform_compat/profiles.py +217 -0
- tree_sitter_analyzer/platform_compat/record.py +55 -0
- tree_sitter_analyzer/platform_compat/recorder.py +155 -0
- tree_sitter_analyzer/platform_compat/report.py +92 -0
- tree_sitter_analyzer/plugins/__init__.py +280 -0
- tree_sitter_analyzer/plugins/base.py +647 -0
- tree_sitter_analyzer/plugins/manager.py +384 -0
- tree_sitter_analyzer/project_detector.py +328 -0
- tree_sitter_analyzer/queries/__init__.py +27 -0
- tree_sitter_analyzer/queries/csharp.py +216 -0
- tree_sitter_analyzer/queries/css.py +615 -0
- tree_sitter_analyzer/queries/go.py +275 -0
- tree_sitter_analyzer/queries/html.py +543 -0
- tree_sitter_analyzer/queries/java.py +402 -0
- tree_sitter_analyzer/queries/javascript.py +724 -0
- tree_sitter_analyzer/queries/kotlin.py +192 -0
- tree_sitter_analyzer/queries/markdown.py +258 -0
- tree_sitter_analyzer/queries/php.py +95 -0
- tree_sitter_analyzer/queries/python.py +859 -0
- tree_sitter_analyzer/queries/ruby.py +92 -0
- tree_sitter_analyzer/queries/rust.py +223 -0
- tree_sitter_analyzer/queries/sql.py +555 -0
- tree_sitter_analyzer/queries/typescript.py +871 -0
- tree_sitter_analyzer/queries/yaml.py +236 -0
- tree_sitter_analyzer/query_loader.py +272 -0
- tree_sitter_analyzer/security/__init__.py +22 -0
- tree_sitter_analyzer/security/boundary_manager.py +277 -0
- tree_sitter_analyzer/security/regex_checker.py +297 -0
- tree_sitter_analyzer/security/validator.py +599 -0
- tree_sitter_analyzer/table_formatter.py +782 -0
- tree_sitter_analyzer/utils/__init__.py +53 -0
- tree_sitter_analyzer/utils/logging.py +433 -0
- tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/entry_points.txt +25 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Kotlin Language Plugin
|
|
4
|
+
|
|
5
|
+
Provides Kotlin-specific parsing and element extraction functionality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
import tree_sitter
|
|
12
|
+
|
|
13
|
+
from ..core.analysis_engine import AnalysisRequest
|
|
14
|
+
from ..models import AnalysisResult
|
|
15
|
+
|
|
16
|
+
from ..encoding_utils import extract_text_slice, safe_encode
|
|
17
|
+
from ..models import Class, Function, Import, Package, Variable
|
|
18
|
+
from ..plugins.base import ElementExtractor, LanguagePlugin
|
|
19
|
+
from ..utils import log_debug, log_error
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class KotlinElementExtractor(ElementExtractor):
|
|
23
|
+
"""Kotlin-specific element extractor"""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
"""Initialize the Kotlin element extractor."""
|
|
27
|
+
self.current_package: str = ""
|
|
28
|
+
self.current_file: str = ""
|
|
29
|
+
self.source_code: str = ""
|
|
30
|
+
self.content_lines: list[str] = []
|
|
31
|
+
self._node_text_cache: dict[int, str] = {}
|
|
32
|
+
|
|
33
|
+
def extract_functions(
|
|
34
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
35
|
+
) -> list[Function]:
|
|
36
|
+
"""Extract Kotlin function declarations"""
|
|
37
|
+
self.source_code = source_code
|
|
38
|
+
self.content_lines = source_code.split("\n")
|
|
39
|
+
self._reset_caches()
|
|
40
|
+
|
|
41
|
+
functions: list[Function] = []
|
|
42
|
+
|
|
43
|
+
self._traverse_and_extract(
|
|
44
|
+
tree.root_node,
|
|
45
|
+
{"function_declaration": self._extract_function},
|
|
46
|
+
functions,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
log_debug(f"Extracted {len(functions)} Kotlin functions")
|
|
50
|
+
return functions
|
|
51
|
+
|
|
52
|
+
def extract_classes(
|
|
53
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
54
|
+
) -> list[Class]:
|
|
55
|
+
"""Extract Kotlin class declarations"""
|
|
56
|
+
self.source_code = source_code
|
|
57
|
+
self.content_lines = source_code.split("\n")
|
|
58
|
+
self._reset_caches()
|
|
59
|
+
|
|
60
|
+
# Extract package
|
|
61
|
+
self._extract_package(tree.root_node)
|
|
62
|
+
|
|
63
|
+
classes: list[Class] = []
|
|
64
|
+
|
|
65
|
+
extractors = {
|
|
66
|
+
"class_declaration": self._extract_class,
|
|
67
|
+
"object_declaration": self._extract_object,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
self._traverse_and_extract(
|
|
71
|
+
tree.root_node,
|
|
72
|
+
extractors,
|
|
73
|
+
classes,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
log_debug(f"Extracted {len(classes)} Kotlin classes")
|
|
77
|
+
return classes
|
|
78
|
+
|
|
79
|
+
def extract_variables(
|
|
80
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
81
|
+
) -> list[Variable]:
|
|
82
|
+
"""Extract Kotlin properties"""
|
|
83
|
+
self.source_code = source_code
|
|
84
|
+
self.content_lines = source_code.split("\n")
|
|
85
|
+
self._reset_caches()
|
|
86
|
+
|
|
87
|
+
variables: list[Variable] = []
|
|
88
|
+
|
|
89
|
+
extractors = {
|
|
90
|
+
"property_declaration": self._extract_property,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
self._traverse_and_extract(
|
|
94
|
+
tree.root_node,
|
|
95
|
+
extractors,
|
|
96
|
+
variables,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
log_debug(f"Extracted {len(variables)} Kotlin properties")
|
|
100
|
+
return variables
|
|
101
|
+
|
|
102
|
+
def extract_imports(
|
|
103
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
104
|
+
) -> list[Import]:
|
|
105
|
+
"""Extract Kotlin imports"""
|
|
106
|
+
self.source_code = source_code
|
|
107
|
+
self.content_lines = source_code.split("\n")
|
|
108
|
+
self._reset_caches()
|
|
109
|
+
|
|
110
|
+
imports: list[Import] = []
|
|
111
|
+
|
|
112
|
+
extractors = {
|
|
113
|
+
"import_header": self._extract_import,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
self._traverse_and_extract(
|
|
117
|
+
tree.root_node,
|
|
118
|
+
extractors,
|
|
119
|
+
imports,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
log_debug(f"Extracted {len(imports)} Kotlin imports")
|
|
123
|
+
return imports
|
|
124
|
+
|
|
125
|
+
def extract_packages(
|
|
126
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
127
|
+
) -> list[Package]:
|
|
128
|
+
"""Extract Kotlin package"""
|
|
129
|
+
self.source_code = source_code
|
|
130
|
+
self.content_lines = source_code.split("\n")
|
|
131
|
+
self._reset_caches()
|
|
132
|
+
|
|
133
|
+
packages: list[Package] = []
|
|
134
|
+
self._extract_package(tree.root_node)
|
|
135
|
+
if self.current_package:
|
|
136
|
+
# Find package node if needed for lines, or just create from string
|
|
137
|
+
# We'll try to find the package_header node
|
|
138
|
+
for child in tree.root_node.children:
|
|
139
|
+
if child.type == "package_header":
|
|
140
|
+
pkg = Package(
|
|
141
|
+
name=self.current_package,
|
|
142
|
+
start_line=child.start_point[0] + 1,
|
|
143
|
+
end_line=child.end_point[0] + 1,
|
|
144
|
+
raw_text=self._get_node_text(child),
|
|
145
|
+
language="kotlin",
|
|
146
|
+
)
|
|
147
|
+
packages.append(pkg)
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
return packages
|
|
151
|
+
|
|
152
|
+
def _reset_caches(self) -> None:
|
|
153
|
+
"""Reset performance caches"""
|
|
154
|
+
self._node_text_cache.clear()
|
|
155
|
+
# Keep current_package if already extracted?
|
|
156
|
+
# Usually safe to re-extract or clear.
|
|
157
|
+
if not self.source_code:
|
|
158
|
+
self.current_package = ""
|
|
159
|
+
|
|
160
|
+
def _traverse_and_extract(
|
|
161
|
+
self,
|
|
162
|
+
node: "tree_sitter.Node",
|
|
163
|
+
extractors: dict[str, Any],
|
|
164
|
+
results: list[Any],
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Recursive traversal to find and extract elements"""
|
|
167
|
+
if node.type in extractors:
|
|
168
|
+
element = extractors[node.type](node)
|
|
169
|
+
if element:
|
|
170
|
+
results.append(element)
|
|
171
|
+
|
|
172
|
+
for child in node.children:
|
|
173
|
+
self._traverse_and_extract(child, extractors, results)
|
|
174
|
+
|
|
175
|
+
def _extract_package(self, node: "tree_sitter.Node") -> None:
|
|
176
|
+
"""Extract package declaration"""
|
|
177
|
+
# Find package_header at top level usually
|
|
178
|
+
for child in node.children:
|
|
179
|
+
if child.type == "package_header":
|
|
180
|
+
# Check children for identifier
|
|
181
|
+
# package_header -> (package) (identifier)
|
|
182
|
+
for grandchild in child.children:
|
|
183
|
+
if (
|
|
184
|
+
grandchild.type == "identifier"
|
|
185
|
+
or grandchild.type == "simple_identifier"
|
|
186
|
+
):
|
|
187
|
+
self.current_package = self._get_node_text(grandchild)
|
|
188
|
+
return
|
|
189
|
+
# Or maybe deeper if qualified name
|
|
190
|
+
if "identifier" in grandchild.type:
|
|
191
|
+
self.current_package = self._get_node_text(grandchild)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
def _extract_function(self, node: "tree_sitter.Node") -> Function | None:
|
|
195
|
+
"""Extract function information"""
|
|
196
|
+
try:
|
|
197
|
+
# name: simple_identifier
|
|
198
|
+
name = "anonymous"
|
|
199
|
+
# Try getting by field name first
|
|
200
|
+
name_node = node.child_by_field_name("name")
|
|
201
|
+
if name_node:
|
|
202
|
+
name = self._get_node_text(name_node)
|
|
203
|
+
else:
|
|
204
|
+
# Fallback to simple_identifier search
|
|
205
|
+
for child in node.children:
|
|
206
|
+
if child.type == "simple_identifier":
|
|
207
|
+
name = self._get_node_text(child)
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
start_line = node.start_point[0] + 1
|
|
211
|
+
end_line = node.end_point[0] + 1
|
|
212
|
+
|
|
213
|
+
# Parameters
|
|
214
|
+
parameters = []
|
|
215
|
+
params_node = node.child_by_field_name(
|
|
216
|
+
"parameters"
|
|
217
|
+
) # function_value_parameters
|
|
218
|
+
if params_node:
|
|
219
|
+
for child in params_node.children:
|
|
220
|
+
if child.type == "parameter":
|
|
221
|
+
# parameter -> simple_identifier: type
|
|
222
|
+
param_name = ""
|
|
223
|
+
param_type = ""
|
|
224
|
+
for grandchild in child.children:
|
|
225
|
+
if grandchild.type == "simple_identifier":
|
|
226
|
+
param_name = self._get_node_text(grandchild)
|
|
227
|
+
elif (
|
|
228
|
+
"type" in grandchild.type
|
|
229
|
+
or grandchild.type == "user_type"
|
|
230
|
+
):
|
|
231
|
+
param_type = self._get_node_text(grandchild)
|
|
232
|
+
|
|
233
|
+
if param_name:
|
|
234
|
+
parameters.append(
|
|
235
|
+
{"name": param_name, "type": param_type or "Any"}
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Return type
|
|
239
|
+
return_type = "Unit"
|
|
240
|
+
# search for return type, usually after :
|
|
241
|
+
# function_declaration -> ... (type)? ...
|
|
242
|
+
# Hard to find specific field without query, iterating children
|
|
243
|
+
# If we find a colon, next child might be type?
|
|
244
|
+
# Tree-sitter-kotlin structure: function_declaration can have children: modifiers, fun, simple_identifier, function_value_parameters, type (return type), function_body
|
|
245
|
+
|
|
246
|
+
for i, child in enumerate(node.children):
|
|
247
|
+
if child.type == ":":
|
|
248
|
+
# Next sibling should be return type
|
|
249
|
+
if i + 1 < len(node.children):
|
|
250
|
+
return_type = self._get_node_text(node.children[i + 1])
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
# Visibility and modifiers
|
|
254
|
+
visibility = "public"
|
|
255
|
+
is_suspend = False
|
|
256
|
+
modifiers_node = node.child_by_field_name("modifiers")
|
|
257
|
+
if modifiers_node:
|
|
258
|
+
mods = self._get_node_text(modifiers_node)
|
|
259
|
+
if "private" in mods:
|
|
260
|
+
visibility = "private"
|
|
261
|
+
elif "protected" in mods:
|
|
262
|
+
visibility = "protected"
|
|
263
|
+
elif "internal" in mods:
|
|
264
|
+
visibility = "internal"
|
|
265
|
+
|
|
266
|
+
if "suspend" in mods:
|
|
267
|
+
is_suspend = True
|
|
268
|
+
|
|
269
|
+
# Docstring
|
|
270
|
+
docstring = self._extract_docstring(node)
|
|
271
|
+
|
|
272
|
+
raw_text = self._get_node_text(node)
|
|
273
|
+
|
|
274
|
+
func = Function(
|
|
275
|
+
name=name,
|
|
276
|
+
start_line=start_line,
|
|
277
|
+
end_line=end_line,
|
|
278
|
+
raw_text=raw_text,
|
|
279
|
+
language="kotlin",
|
|
280
|
+
parameters=parameters,
|
|
281
|
+
return_type=return_type,
|
|
282
|
+
visibility=visibility,
|
|
283
|
+
docstring=docstring,
|
|
284
|
+
)
|
|
285
|
+
func.is_suspend = is_suspend
|
|
286
|
+
return func
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
log_error(f"Error extracting Kotlin function: {e}")
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
def _extract_class(self, node: "tree_sitter.Node") -> Class | None:
|
|
293
|
+
"""Extract class declaration"""
|
|
294
|
+
return self._extract_class_or_object(node, "class")
|
|
295
|
+
|
|
296
|
+
def _extract_object(self, node: "tree_sitter.Node") -> Class | None:
|
|
297
|
+
"""Extract object declaration"""
|
|
298
|
+
return self._extract_class_or_object(node, "object")
|
|
299
|
+
|
|
300
|
+
def _extract_class_or_object(
|
|
301
|
+
self, node: "tree_sitter.Node", kind: str
|
|
302
|
+
) -> Class | None:
|
|
303
|
+
"""Generic extraction for class/object/interface"""
|
|
304
|
+
try:
|
|
305
|
+
name = "anonymous"
|
|
306
|
+
# Try getting by field name first
|
|
307
|
+
name_node = node.child_by_field_name("name")
|
|
308
|
+
if name_node:
|
|
309
|
+
name = self._get_node_text(name_node)
|
|
310
|
+
else:
|
|
311
|
+
for child in node.children:
|
|
312
|
+
if child.type == "simple_identifier":
|
|
313
|
+
name = self._get_node_text(child)
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
start_line = node.start_point[0] + 1
|
|
317
|
+
end_line = node.end_point[0] + 1
|
|
318
|
+
|
|
319
|
+
visibility = "public"
|
|
320
|
+
modifiers_node = node.child_by_field_name("modifiers")
|
|
321
|
+
if modifiers_node:
|
|
322
|
+
mods = self._get_node_text(modifiers_node)
|
|
323
|
+
if "private" in mods:
|
|
324
|
+
visibility = "private"
|
|
325
|
+
elif "protected" in mods:
|
|
326
|
+
visibility = "protected"
|
|
327
|
+
elif "internal" in mods:
|
|
328
|
+
visibility = "internal"
|
|
329
|
+
|
|
330
|
+
# Detect interface by checking for 'interface' keyword child node
|
|
331
|
+
# tree-sitter-kotlin parses both class and interface as class_declaration
|
|
332
|
+
# but includes 'interface' or 'class' keyword as a child node
|
|
333
|
+
if kind == "class":
|
|
334
|
+
for child in node.children:
|
|
335
|
+
if child.type == "interface":
|
|
336
|
+
kind = "interface"
|
|
337
|
+
break
|
|
338
|
+
elif child.type == "class":
|
|
339
|
+
# Explicitly a class, not interface
|
|
340
|
+
break
|
|
341
|
+
|
|
342
|
+
raw_text = self._get_node_text(node)
|
|
343
|
+
|
|
344
|
+
return Class(
|
|
345
|
+
name=name,
|
|
346
|
+
start_line=start_line,
|
|
347
|
+
end_line=end_line,
|
|
348
|
+
raw_text=raw_text,
|
|
349
|
+
language="kotlin",
|
|
350
|
+
class_type=kind,
|
|
351
|
+
visibility=visibility,
|
|
352
|
+
package_name=self.current_package,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
except Exception as e:
|
|
356
|
+
log_error(f"Error extracting Kotlin class: {e}")
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
def _extract_property(self, node: "tree_sitter.Node") -> Variable | None:
|
|
360
|
+
"""Extract property declaration"""
|
|
361
|
+
try:
|
|
362
|
+
# var declaration or val declaration
|
|
363
|
+
is_val = False
|
|
364
|
+
is_var = False
|
|
365
|
+
text = self._get_node_text(node)
|
|
366
|
+
if text.startswith("val "):
|
|
367
|
+
is_val = True
|
|
368
|
+
elif text.startswith("var "):
|
|
369
|
+
is_var = True
|
|
370
|
+
|
|
371
|
+
# variable_declaration -> (modifiers)? (val/var) ...
|
|
372
|
+
# Need to find name
|
|
373
|
+
name = "unknown"
|
|
374
|
+
|
|
375
|
+
# Try getting by field name 'name' directly on property_declaration (might work in newer grammars)
|
|
376
|
+
name_node = node.child_by_field_name("name")
|
|
377
|
+
if name_node:
|
|
378
|
+
name = self._get_node_text(name_node)
|
|
379
|
+
else:
|
|
380
|
+
# Fallback: Iterate children
|
|
381
|
+
for child in node.children:
|
|
382
|
+
if child.type == "variable_declaration":
|
|
383
|
+
for grandchild in child.children:
|
|
384
|
+
if grandchild.type == "simple_identifier":
|
|
385
|
+
name = self._get_node_text(grandchild)
|
|
386
|
+
break
|
|
387
|
+
elif child.type == "simple_identifier":
|
|
388
|
+
name = self._get_node_text(child)
|
|
389
|
+
break
|
|
390
|
+
|
|
391
|
+
start_line = node.start_point[0] + 1
|
|
392
|
+
end_line = node.end_point[0] + 1
|
|
393
|
+
|
|
394
|
+
# Type?
|
|
395
|
+
prop_type = "Inferred"
|
|
396
|
+
# Look for : type
|
|
397
|
+
|
|
398
|
+
visibility = "public"
|
|
399
|
+
modifiers_node = node.child_by_field_name("modifiers")
|
|
400
|
+
if modifiers_node:
|
|
401
|
+
mods = self._get_node_text(modifiers_node)
|
|
402
|
+
if "private" in mods:
|
|
403
|
+
visibility = "private"
|
|
404
|
+
|
|
405
|
+
docstring = self._extract_docstring(node)
|
|
406
|
+
raw_text = self._get_node_text(node)
|
|
407
|
+
|
|
408
|
+
var = Variable(
|
|
409
|
+
name=name,
|
|
410
|
+
start_line=start_line,
|
|
411
|
+
end_line=end_line,
|
|
412
|
+
raw_text=raw_text,
|
|
413
|
+
language="kotlin",
|
|
414
|
+
variable_type=prop_type,
|
|
415
|
+
visibility=visibility,
|
|
416
|
+
docstring=docstring,
|
|
417
|
+
)
|
|
418
|
+
var.is_val = is_val
|
|
419
|
+
var.is_var = is_var
|
|
420
|
+
|
|
421
|
+
return var
|
|
422
|
+
|
|
423
|
+
except Exception as e:
|
|
424
|
+
log_error(f"Error extracting Kotlin property: {e}")
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
def _extract_import(self, node: "tree_sitter.Node") -> Import | None:
|
|
428
|
+
"""Extract import header"""
|
|
429
|
+
try:
|
|
430
|
+
# import_header -> 'import' identifier .*
|
|
431
|
+
raw_text = self._get_node_text(node)
|
|
432
|
+
start_line = node.start_point[0] + 1
|
|
433
|
+
end_line = node.end_point[0] + 1
|
|
434
|
+
|
|
435
|
+
# Parse name
|
|
436
|
+
parts = raw_text.split()
|
|
437
|
+
if len(parts) > 1:
|
|
438
|
+
name = parts[1]
|
|
439
|
+
else:
|
|
440
|
+
name = "unknown"
|
|
441
|
+
|
|
442
|
+
return Import(
|
|
443
|
+
name=name,
|
|
444
|
+
start_line=start_line,
|
|
445
|
+
end_line=end_line,
|
|
446
|
+
raw_text=raw_text,
|
|
447
|
+
language="kotlin",
|
|
448
|
+
import_statement=raw_text,
|
|
449
|
+
)
|
|
450
|
+
except Exception as e:
|
|
451
|
+
log_error(f"Error extracting Kotlin import: {e}")
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
def _get_node_text(self, node: "tree_sitter.Node") -> str:
|
|
455
|
+
"""Get node text with caching"""
|
|
456
|
+
node_id = id(node)
|
|
457
|
+
if node_id in self._node_text_cache:
|
|
458
|
+
return self._node_text_cache[node_id]
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
start_byte = node.start_byte
|
|
462
|
+
end_byte = node.end_byte
|
|
463
|
+
encoding = "utf-8"
|
|
464
|
+
content_bytes = safe_encode("\n".join(self.content_lines), encoding)
|
|
465
|
+
text = extract_text_slice(content_bytes, start_byte, end_byte, encoding)
|
|
466
|
+
self._node_text_cache[node_id] = text
|
|
467
|
+
return text
|
|
468
|
+
except Exception:
|
|
469
|
+
return ""
|
|
470
|
+
|
|
471
|
+
def _extract_docstring(self, node: "tree_sitter.Node") -> str | None:
|
|
472
|
+
"""Extract KDoc"""
|
|
473
|
+
# Similar to Rust/Java logic
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class KotlinPlugin(LanguagePlugin):
|
|
478
|
+
"""Kotlin language plugin implementation"""
|
|
479
|
+
|
|
480
|
+
def __init__(self) -> None:
|
|
481
|
+
"""Initialize the Kotlin language plugin."""
|
|
482
|
+
super().__init__()
|
|
483
|
+
self.extractor = KotlinElementExtractor()
|
|
484
|
+
self.language = "kotlin"
|
|
485
|
+
self.supported_extensions = self.get_file_extensions()
|
|
486
|
+
self._cached_language: Any | None = None
|
|
487
|
+
|
|
488
|
+
def get_language_name(self) -> str:
|
|
489
|
+
"""Get the language name."""
|
|
490
|
+
return "kotlin"
|
|
491
|
+
|
|
492
|
+
def get_file_extensions(self) -> list[str]:
|
|
493
|
+
"""Get supported file extensions."""
|
|
494
|
+
return [".kt", ".kts"]
|
|
495
|
+
|
|
496
|
+
def create_extractor(self) -> ElementExtractor:
|
|
497
|
+
"""Create a new element extractor instance."""
|
|
498
|
+
return KotlinElementExtractor()
|
|
499
|
+
|
|
500
|
+
async def analyze_file(
|
|
501
|
+
self, file_path: str, request: "AnalysisRequest"
|
|
502
|
+
) -> "AnalysisResult":
|
|
503
|
+
"""Analyze Kotlin code and return structured results."""
|
|
504
|
+
|
|
505
|
+
from ..models import AnalysisResult
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
from ..encoding_utils import read_file_safe
|
|
509
|
+
|
|
510
|
+
file_content, detected_encoding = read_file_safe(file_path)
|
|
511
|
+
|
|
512
|
+
# Get tree-sitter language and parse
|
|
513
|
+
language = self.get_tree_sitter_language()
|
|
514
|
+
if language is None:
|
|
515
|
+
return AnalysisResult(
|
|
516
|
+
file_path=file_path,
|
|
517
|
+
language="kotlin",
|
|
518
|
+
line_count=len(file_content.split("\n")),
|
|
519
|
+
elements=[],
|
|
520
|
+
source_code=file_content,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
import tree_sitter
|
|
524
|
+
|
|
525
|
+
parser = tree_sitter.Parser()
|
|
526
|
+
|
|
527
|
+
# Set language
|
|
528
|
+
if hasattr(parser, "set_language"):
|
|
529
|
+
parser.set_language(language)
|
|
530
|
+
elif hasattr(parser, "language"):
|
|
531
|
+
parser.language = language
|
|
532
|
+
else:
|
|
533
|
+
parser = tree_sitter.Parser(language)
|
|
534
|
+
|
|
535
|
+
tree = parser.parse(file_content.encode("utf-8"))
|
|
536
|
+
|
|
537
|
+
# Extract elements
|
|
538
|
+
elements_dict = self.extract_elements(tree, file_content)
|
|
539
|
+
|
|
540
|
+
all_elements = []
|
|
541
|
+
all_elements.extend(elements_dict.get("functions", []))
|
|
542
|
+
all_elements.extend(elements_dict.get("classes", []))
|
|
543
|
+
all_elements.extend(elements_dict.get("variables", []))
|
|
544
|
+
all_elements.extend(elements_dict.get("imports", []))
|
|
545
|
+
all_elements.extend(elements_dict.get("packages", []))
|
|
546
|
+
|
|
547
|
+
node_count = (
|
|
548
|
+
self._count_tree_nodes(tree.root_node) if tree and tree.root_node else 0
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Get package
|
|
552
|
+
package = (
|
|
553
|
+
elements_dict.get("packages", [])[0]
|
|
554
|
+
if elements_dict.get("packages")
|
|
555
|
+
else None
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
return AnalysisResult(
|
|
559
|
+
file_path=file_path,
|
|
560
|
+
language="kotlin",
|
|
561
|
+
line_count=len(file_content.split("\n")),
|
|
562
|
+
elements=all_elements,
|
|
563
|
+
node_count=node_count,
|
|
564
|
+
source_code=file_content,
|
|
565
|
+
package=package,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
except Exception as e:
|
|
569
|
+
log_error(f"Error analyzing Kotlin file {file_path}: {e}")
|
|
570
|
+
return AnalysisResult(
|
|
571
|
+
file_path=file_path,
|
|
572
|
+
language="kotlin",
|
|
573
|
+
line_count=0,
|
|
574
|
+
elements=[],
|
|
575
|
+
source_code="",
|
|
576
|
+
error_message=str(e),
|
|
577
|
+
success=False,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
def _count_tree_nodes(self, node: Any) -> int:
|
|
581
|
+
"""Recursively count nodes."""
|
|
582
|
+
if node is None:
|
|
583
|
+
return 0
|
|
584
|
+
count = 1
|
|
585
|
+
if hasattr(node, "children"):
|
|
586
|
+
for child in node.children:
|
|
587
|
+
count += self._count_tree_nodes(child)
|
|
588
|
+
return count
|
|
589
|
+
|
|
590
|
+
def get_tree_sitter_language(self) -> Any | None:
|
|
591
|
+
"""Get the tree-sitter language for Kotlin."""
|
|
592
|
+
if self._cached_language is not None:
|
|
593
|
+
return self._cached_language
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
import tree_sitter
|
|
597
|
+
import tree_sitter_kotlin
|
|
598
|
+
|
|
599
|
+
caps_or_lang = tree_sitter_kotlin.language()
|
|
600
|
+
|
|
601
|
+
if hasattr(caps_or_lang, "__class__") and "Language" in str(
|
|
602
|
+
type(caps_or_lang)
|
|
603
|
+
):
|
|
604
|
+
self._cached_language = caps_or_lang
|
|
605
|
+
else:
|
|
606
|
+
try:
|
|
607
|
+
self._cached_language = tree_sitter.Language(caps_or_lang)
|
|
608
|
+
except Exception as e:
|
|
609
|
+
log_error(f"Failed to create Language object: {e}")
|
|
610
|
+
return None
|
|
611
|
+
|
|
612
|
+
return self._cached_language
|
|
613
|
+
except ImportError as e:
|
|
614
|
+
log_error(f"tree-sitter-kotlin not available: {e}")
|
|
615
|
+
return None
|
|
616
|
+
except Exception as e:
|
|
617
|
+
log_error(f"Failed to load tree-sitter language for Kotlin: {e}")
|
|
618
|
+
return None
|
|
619
|
+
|
|
620
|
+
def extract_elements(self, tree: Any | None, source_code: str) -> dict[str, Any]:
|
|
621
|
+
"""Extract all elements."""
|
|
622
|
+
if tree is None:
|
|
623
|
+
return {
|
|
624
|
+
"functions": [],
|
|
625
|
+
"classes": [],
|
|
626
|
+
"variables": [],
|
|
627
|
+
"imports": [],
|
|
628
|
+
"packages": [],
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
try:
|
|
632
|
+
extractor = self.create_extractor()
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
"functions": extractor.extract_functions(tree, source_code),
|
|
636
|
+
"classes": extractor.extract_classes(tree, source_code),
|
|
637
|
+
"variables": extractor.extract_variables(tree, source_code),
|
|
638
|
+
"imports": extractor.extract_imports(tree, source_code),
|
|
639
|
+
"packages": extractor.extract_packages(tree, source_code),
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
except Exception as e:
|
|
643
|
+
log_error(f"Error extracting elements: {e}")
|
|
644
|
+
return {
|
|
645
|
+
"functions": [],
|
|
646
|
+
"classes": [],
|
|
647
|
+
"variables": [],
|
|
648
|
+
"imports": [],
|
|
649
|
+
"packages": [],
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
def supports_file(self, file_path: str) -> bool:
|
|
653
|
+
"""Check if this plugin supports the given file."""
|
|
654
|
+
return any(
|
|
655
|
+
file_path.lower().endswith(ext) for ext in self.get_file_extensions()
|
|
656
|
+
)
|