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,757 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Ruby Language Plugin
|
|
4
|
+
|
|
5
|
+
Provides Ruby-specific parsing and element extraction functionality.
|
|
6
|
+
Supports extraction of classes, modules, methods, constants, variables, and require statements.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import tree_sitter
|
|
13
|
+
|
|
14
|
+
from ..core.analysis_engine import AnalysisRequest
|
|
15
|
+
from ..models import AnalysisResult
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import tree_sitter
|
|
19
|
+
|
|
20
|
+
TREE_SITTER_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
TREE_SITTER_AVAILABLE = False
|
|
23
|
+
|
|
24
|
+
from ..models import Class, Function, Import, Variable
|
|
25
|
+
from ..plugins.base import ElementExtractor, LanguagePlugin
|
|
26
|
+
from ..utils import log_error
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RubyElementExtractor(ElementExtractor):
|
|
30
|
+
"""
|
|
31
|
+
Ruby-specific element extractor.
|
|
32
|
+
|
|
33
|
+
This extractor parses Ruby AST and extracts code elements, mapping them
|
|
34
|
+
to the unified element model:
|
|
35
|
+
- Classes, Modules → Class elements
|
|
36
|
+
- Methods (instance and class methods) → Function elements
|
|
37
|
+
- Constants, Instance variables, Class variables → Variable elements
|
|
38
|
+
- Require statements → Import elements
|
|
39
|
+
|
|
40
|
+
The extractor handles modern Ruby syntax including:
|
|
41
|
+
- Ruby 3+ features
|
|
42
|
+
- Blocks, procs, and lambdas
|
|
43
|
+
- attr_accessor, attr_reader, attr_writer
|
|
44
|
+
- Symbols and string interpolation
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Initialize the Ruby element extractor.
|
|
50
|
+
|
|
51
|
+
Sets up internal state for source code processing and performance
|
|
52
|
+
optimization caches for node text extraction.
|
|
53
|
+
"""
|
|
54
|
+
super().__init__()
|
|
55
|
+
self.source_code: str = ""
|
|
56
|
+
self.content_lines: list[str] = []
|
|
57
|
+
self.current_module: str = ""
|
|
58
|
+
|
|
59
|
+
# Performance optimization caches
|
|
60
|
+
self._node_text_cache: dict[int, str] = {}
|
|
61
|
+
self._processed_nodes: set[int] = set()
|
|
62
|
+
self._element_cache: dict[tuple[int, str], Any] = {}
|
|
63
|
+
self._file_encoding: str | None = None
|
|
64
|
+
|
|
65
|
+
def _reset_caches(self) -> None:
|
|
66
|
+
"""Reset all internal caches for a new file analysis."""
|
|
67
|
+
self._node_text_cache.clear()
|
|
68
|
+
self._processed_nodes.clear()
|
|
69
|
+
self._element_cache.clear()
|
|
70
|
+
self.current_module = ""
|
|
71
|
+
|
|
72
|
+
def _get_node_text_optimized(self, node: "tree_sitter.Node") -> str:
|
|
73
|
+
"""
|
|
74
|
+
Get text content of a node with caching for performance.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
node: Tree-sitter node to extract text from
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Text content of the node as string
|
|
81
|
+
"""
|
|
82
|
+
# Use node position as cache key instead of object id for deterministic behavior
|
|
83
|
+
cache_key = (node.start_byte, node.end_byte)
|
|
84
|
+
if cache_key in self._node_text_cache:
|
|
85
|
+
return self._node_text_cache[cache_key]
|
|
86
|
+
|
|
87
|
+
# Extract text directly from source code string
|
|
88
|
+
text = self.source_code[node.start_byte : node.end_byte]
|
|
89
|
+
self._node_text_cache[cache_key] = text
|
|
90
|
+
return text
|
|
91
|
+
|
|
92
|
+
def _determine_visibility(self, node: "tree_sitter.Node") -> str:
|
|
93
|
+
"""
|
|
94
|
+
Determine visibility of a method.
|
|
95
|
+
|
|
96
|
+
In Ruby, visibility is typically set using private, protected, public keywords.
|
|
97
|
+
Default is public for methods.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
node: Method node
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Visibility string ("public", "private", "protected")
|
|
104
|
+
"""
|
|
105
|
+
# TODO: Implement visibility detection by looking for visibility modifiers
|
|
106
|
+
# For now, default to public
|
|
107
|
+
return "public"
|
|
108
|
+
|
|
109
|
+
def extract_classes(
|
|
110
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
111
|
+
) -> list[Class]:
|
|
112
|
+
"""
|
|
113
|
+
Extract Ruby classes and modules.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
tree: Parsed tree-sitter tree
|
|
117
|
+
source_code: Source code string
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of Class elements
|
|
121
|
+
"""
|
|
122
|
+
self.source_code = source_code
|
|
123
|
+
self.content_lines = source_code.splitlines()
|
|
124
|
+
self._reset_caches()
|
|
125
|
+
|
|
126
|
+
classes: list[Class] = []
|
|
127
|
+
|
|
128
|
+
# Iterative traversal to avoid stack overflow
|
|
129
|
+
stack: list[tree_sitter.Node] = [tree.root_node]
|
|
130
|
+
|
|
131
|
+
while stack:
|
|
132
|
+
node = stack.pop()
|
|
133
|
+
|
|
134
|
+
if node.type in ("class", "module"):
|
|
135
|
+
class_elem = self._extract_class_element(node)
|
|
136
|
+
if class_elem:
|
|
137
|
+
classes.append(class_elem)
|
|
138
|
+
|
|
139
|
+
# Add children to stack for traversal
|
|
140
|
+
for child in reversed(node.children):
|
|
141
|
+
stack.append(child)
|
|
142
|
+
|
|
143
|
+
return classes
|
|
144
|
+
|
|
145
|
+
def _extract_class_element(self, node: "tree_sitter.Node") -> Class | None:
|
|
146
|
+
"""
|
|
147
|
+
Extract a single class or module element.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
node: Class/module node
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Class element or None if extraction fails
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
# Extract name
|
|
157
|
+
name_node = None
|
|
158
|
+
for child in node.children:
|
|
159
|
+
if child.type in ("constant", "scope_resolution"):
|
|
160
|
+
name_node = child
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
if not name_node:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
name = self._get_node_text_optimized(name_node)
|
|
167
|
+
is_module = node.type == "module"
|
|
168
|
+
|
|
169
|
+
# Extract superclass for classes
|
|
170
|
+
base_classes: list[str] = []
|
|
171
|
+
for child in node.children:
|
|
172
|
+
if child.type == "superclass":
|
|
173
|
+
superclass_node = child.children[0] if child.children else None
|
|
174
|
+
if superclass_node:
|
|
175
|
+
base_classes.append(
|
|
176
|
+
self._get_node_text_optimized(superclass_node)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return Class(
|
|
180
|
+
name=name,
|
|
181
|
+
start_line=node.start_point[0] + 1,
|
|
182
|
+
end_line=node.end_point[0] + 1,
|
|
183
|
+
visibility="public",
|
|
184
|
+
is_abstract=False,
|
|
185
|
+
full_qualified_name=name,
|
|
186
|
+
superclass=base_classes[0] if base_classes else None,
|
|
187
|
+
interfaces=[],
|
|
188
|
+
modifiers=[],
|
|
189
|
+
annotations=[],
|
|
190
|
+
class_type="module" if is_module else "class",
|
|
191
|
+
)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
log_error(f"Error extracting class element: {e}")
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def extract_functions(
|
|
197
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
198
|
+
) -> list[Function]:
|
|
199
|
+
"""
|
|
200
|
+
Extract Ruby methods.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
tree: Parsed tree-sitter tree
|
|
204
|
+
source_code: Source code string
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
List of Function elements
|
|
208
|
+
"""
|
|
209
|
+
self.source_code = source_code
|
|
210
|
+
self.content_lines = source_code.splitlines()
|
|
211
|
+
|
|
212
|
+
functions: list[Function] = []
|
|
213
|
+
|
|
214
|
+
# Iterative traversal
|
|
215
|
+
stack: list[tuple[tree_sitter.Node, str]] = [(tree.root_node, "")]
|
|
216
|
+
|
|
217
|
+
while stack:
|
|
218
|
+
node, parent_class = stack.pop()
|
|
219
|
+
|
|
220
|
+
if node.type == "method":
|
|
221
|
+
func_elem = self._extract_method_element(node, parent_class)
|
|
222
|
+
if func_elem:
|
|
223
|
+
functions.append(func_elem)
|
|
224
|
+
elif node.type == "singleton_method":
|
|
225
|
+
func_elem = self._extract_singleton_method_element(node, parent_class)
|
|
226
|
+
if func_elem:
|
|
227
|
+
functions.append(func_elem)
|
|
228
|
+
elif node.type == "call":
|
|
229
|
+
# Check for attr_accessor, attr_reader, attr_writer
|
|
230
|
+
func_elems = self._extract_attr_methods(node, parent_class)
|
|
231
|
+
functions.extend(func_elems)
|
|
232
|
+
|
|
233
|
+
# Track parent class for methods
|
|
234
|
+
new_parent = parent_class
|
|
235
|
+
if node.type in ("class", "module"):
|
|
236
|
+
for child in node.children:
|
|
237
|
+
if child.type in ("constant", "scope_resolution"):
|
|
238
|
+
new_parent = self._get_node_text_optimized(child)
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
# Add children to stack
|
|
242
|
+
for child in reversed(node.children):
|
|
243
|
+
stack.append((child, new_parent))
|
|
244
|
+
|
|
245
|
+
return functions
|
|
246
|
+
|
|
247
|
+
def _extract_method_element(
|
|
248
|
+
self, node: "tree_sitter.Node", parent_class: str
|
|
249
|
+
) -> Function | None:
|
|
250
|
+
"""
|
|
251
|
+
Extract a method element.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
node: Method node
|
|
255
|
+
parent_class: Name of the parent class
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Function element or None if extraction fails
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
# Extract method name
|
|
262
|
+
name_node = node.child_by_field_name("name")
|
|
263
|
+
if not name_node:
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
name = self._get_node_text_optimized(name_node)
|
|
267
|
+
visibility = self._determine_visibility(node)
|
|
268
|
+
|
|
269
|
+
# Extract parameters
|
|
270
|
+
parameters: list[str] = []
|
|
271
|
+
params_node = node.child_by_field_name("parameters")
|
|
272
|
+
if params_node:
|
|
273
|
+
for param in params_node.children:
|
|
274
|
+
if param.type in (
|
|
275
|
+
"identifier",
|
|
276
|
+
"optional_parameter",
|
|
277
|
+
"splat_parameter",
|
|
278
|
+
"hash_splat_parameter",
|
|
279
|
+
"block_parameter",
|
|
280
|
+
):
|
|
281
|
+
param_text = self._get_node_text_optimized(param)
|
|
282
|
+
parameters.append(param_text)
|
|
283
|
+
|
|
284
|
+
return Function(
|
|
285
|
+
name=f"{parent_class}#{name}" if parent_class else name,
|
|
286
|
+
start_line=node.start_point[0] + 1,
|
|
287
|
+
end_line=node.end_point[0] + 1,
|
|
288
|
+
visibility=visibility,
|
|
289
|
+
is_static=False,
|
|
290
|
+
is_async=False,
|
|
291
|
+
is_abstract=False,
|
|
292
|
+
parameters=parameters,
|
|
293
|
+
return_type="",
|
|
294
|
+
modifiers=[],
|
|
295
|
+
annotations=[],
|
|
296
|
+
)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
log_error(f"Error extracting method element: {e}")
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
def _extract_singleton_method_element(
|
|
302
|
+
self, node: "tree_sitter.Node", parent_class: str
|
|
303
|
+
) -> Function | None:
|
|
304
|
+
"""
|
|
305
|
+
Extract a singleton (class) method element.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
node: Singleton method node
|
|
309
|
+
parent_class: Name of the parent class
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Function element or None if extraction fails
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
# Extract method name
|
|
316
|
+
name_node = node.child_by_field_name("name")
|
|
317
|
+
if not name_node:
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
name = self._get_node_text_optimized(name_node)
|
|
321
|
+
visibility = self._determine_visibility(node)
|
|
322
|
+
|
|
323
|
+
# Extract parameters
|
|
324
|
+
parameters: list[str] = []
|
|
325
|
+
params_node = node.child_by_field_name("parameters")
|
|
326
|
+
if params_node:
|
|
327
|
+
for param in params_node.children:
|
|
328
|
+
if param.type in (
|
|
329
|
+
"identifier",
|
|
330
|
+
"optional_parameter",
|
|
331
|
+
"splat_parameter",
|
|
332
|
+
"hash_splat_parameter",
|
|
333
|
+
"block_parameter",
|
|
334
|
+
):
|
|
335
|
+
param_text = self._get_node_text_optimized(param)
|
|
336
|
+
parameters.append(param_text)
|
|
337
|
+
|
|
338
|
+
return Function(
|
|
339
|
+
name=f"{parent_class}.{name}" if parent_class else name,
|
|
340
|
+
start_line=node.start_point[0] + 1,
|
|
341
|
+
end_line=node.end_point[0] + 1,
|
|
342
|
+
visibility=visibility,
|
|
343
|
+
is_static=True, # Singleton methods are class methods
|
|
344
|
+
is_async=False,
|
|
345
|
+
is_abstract=False,
|
|
346
|
+
parameters=parameters,
|
|
347
|
+
return_type="",
|
|
348
|
+
modifiers=[],
|
|
349
|
+
annotations=[],
|
|
350
|
+
)
|
|
351
|
+
except Exception as e:
|
|
352
|
+
log_error(f"Error extracting singleton method element: {e}")
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
def _extract_attr_methods(
|
|
356
|
+
self, node: "tree_sitter.Node", parent_class: str
|
|
357
|
+
) -> list[Function]:
|
|
358
|
+
"""
|
|
359
|
+
Extract attr_accessor, attr_reader, attr_writer methods.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
node: Call node
|
|
363
|
+
parent_class: Name of the parent class
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
List of Function elements
|
|
367
|
+
"""
|
|
368
|
+
functions: list[Function] = []
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
# Check if this is an attr_* call
|
|
372
|
+
method_node = node.child_by_field_name("method")
|
|
373
|
+
if not method_node:
|
|
374
|
+
return functions
|
|
375
|
+
|
|
376
|
+
method_name = self._get_node_text_optimized(method_node)
|
|
377
|
+
if method_name not in ("attr_accessor", "attr_reader", "attr_writer"):
|
|
378
|
+
return functions
|
|
379
|
+
|
|
380
|
+
# Extract attribute names
|
|
381
|
+
args_node = node.child_by_field_name("arguments")
|
|
382
|
+
if not args_node:
|
|
383
|
+
return functions
|
|
384
|
+
|
|
385
|
+
for arg in args_node.children:
|
|
386
|
+
if arg.type == "simple_symbol":
|
|
387
|
+
attr_name = self._get_node_text_optimized(arg).lstrip(":")
|
|
388
|
+
|
|
389
|
+
# Determine read/write permissions
|
|
390
|
+
is_reader = method_name in ("attr_accessor", "attr_reader")
|
|
391
|
+
is_writer = method_name in ("attr_accessor", "attr_writer")
|
|
392
|
+
|
|
393
|
+
# metadata = { # Reserved for future use
|
|
394
|
+
# "parent_class": parent_class,
|
|
395
|
+
# "attr_type": method_name,
|
|
396
|
+
# "is_reader": is_reader,
|
|
397
|
+
# "is_writer": is_writer,
|
|
398
|
+
# }
|
|
399
|
+
_ = (is_reader, is_writer) # Mark as used
|
|
400
|
+
|
|
401
|
+
functions.append(
|
|
402
|
+
Function(
|
|
403
|
+
name=(
|
|
404
|
+
f"{parent_class}#{attr_name}"
|
|
405
|
+
if parent_class
|
|
406
|
+
else attr_name
|
|
407
|
+
),
|
|
408
|
+
start_line=node.start_point[0] + 1,
|
|
409
|
+
end_line=node.end_point[0] + 1,
|
|
410
|
+
visibility="public",
|
|
411
|
+
is_static=False,
|
|
412
|
+
is_async=False,
|
|
413
|
+
is_abstract=False,
|
|
414
|
+
parameters=[],
|
|
415
|
+
return_type="",
|
|
416
|
+
modifiers=[],
|
|
417
|
+
annotations=[],
|
|
418
|
+
is_property=True,
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
except Exception as e:
|
|
422
|
+
log_error(f"Error extracting attr methods: {e}")
|
|
423
|
+
|
|
424
|
+
return functions
|
|
425
|
+
|
|
426
|
+
def extract_variables(
|
|
427
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
428
|
+
) -> list[Variable]:
|
|
429
|
+
"""
|
|
430
|
+
Extract Ruby constants and variables.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
tree: Parsed tree-sitter tree
|
|
434
|
+
source_code: Source code string
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
List of Variable elements
|
|
438
|
+
"""
|
|
439
|
+
self.source_code = source_code
|
|
440
|
+
self.content_lines = source_code.splitlines()
|
|
441
|
+
|
|
442
|
+
variables: list[Variable] = []
|
|
443
|
+
|
|
444
|
+
# Iterative traversal
|
|
445
|
+
stack: list[tuple[tree_sitter.Node, str]] = [(tree.root_node, "")]
|
|
446
|
+
|
|
447
|
+
while stack:
|
|
448
|
+
node, parent_class = stack.pop()
|
|
449
|
+
|
|
450
|
+
if node.type == "assignment":
|
|
451
|
+
var_elem = self._extract_assignment_variable(node, parent_class)
|
|
452
|
+
if var_elem:
|
|
453
|
+
variables.append(var_elem)
|
|
454
|
+
|
|
455
|
+
# Track parent class
|
|
456
|
+
new_parent = parent_class
|
|
457
|
+
if node.type in ("class", "module"):
|
|
458
|
+
for child in node.children:
|
|
459
|
+
if child.type in ("constant", "scope_resolution"):
|
|
460
|
+
new_parent = self._get_node_text_optimized(child)
|
|
461
|
+
break
|
|
462
|
+
|
|
463
|
+
# Add children to stack
|
|
464
|
+
for child in reversed(node.children):
|
|
465
|
+
stack.append((child, new_parent))
|
|
466
|
+
|
|
467
|
+
return variables
|
|
468
|
+
|
|
469
|
+
def _extract_assignment_variable(
|
|
470
|
+
self, node: "tree_sitter.Node", parent_class: str
|
|
471
|
+
) -> Variable | None:
|
|
472
|
+
"""
|
|
473
|
+
Extract variable from assignment.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
node: Assignment node
|
|
477
|
+
parent_class: Name of the parent class
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Variable element or None if extraction fails
|
|
481
|
+
"""
|
|
482
|
+
try:
|
|
483
|
+
# Extract left side (variable name)
|
|
484
|
+
left_node = node.child_by_field_name("left")
|
|
485
|
+
if not left_node:
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
var_text = self._get_node_text_optimized(left_node)
|
|
489
|
+
|
|
490
|
+
# Determine variable type
|
|
491
|
+
is_constant = left_node.type == "constant"
|
|
492
|
+
# is_instance_var = var_text.startswith("@") and not var_text.startswith("@@") # Reserved for future use
|
|
493
|
+
is_class_var = var_text.startswith("@@")
|
|
494
|
+
# is_global = var_text.startswith("$") # Reserved for future use
|
|
495
|
+
|
|
496
|
+
# Clean variable name
|
|
497
|
+
name = var_text.lstrip("@$")
|
|
498
|
+
full_name = (
|
|
499
|
+
f"{parent_class}::{name}" if parent_class and is_constant else name
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
return Variable(
|
|
503
|
+
name=full_name,
|
|
504
|
+
start_line=node.start_point[0] + 1,
|
|
505
|
+
end_line=node.end_point[0] + 1,
|
|
506
|
+
visibility="public" if is_constant else "private",
|
|
507
|
+
is_static=is_class_var or is_constant,
|
|
508
|
+
is_constant=is_constant,
|
|
509
|
+
is_final=is_constant,
|
|
510
|
+
variable_type="",
|
|
511
|
+
modifiers=[],
|
|
512
|
+
)
|
|
513
|
+
except Exception as e:
|
|
514
|
+
log_error(f"Error extracting variable: {e}")
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
def extract_imports(
|
|
518
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
519
|
+
) -> list[Import]:
|
|
520
|
+
"""
|
|
521
|
+
Extract Ruby require statements.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
tree: Parsed tree-sitter tree
|
|
525
|
+
source_code: Source code string
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
List of Import elements
|
|
529
|
+
"""
|
|
530
|
+
self.source_code = source_code
|
|
531
|
+
self.content_lines = source_code.splitlines()
|
|
532
|
+
|
|
533
|
+
imports: list[Import] = []
|
|
534
|
+
|
|
535
|
+
# Iterative traversal
|
|
536
|
+
stack: list[tree_sitter.Node] = [tree.root_node]
|
|
537
|
+
|
|
538
|
+
while stack:
|
|
539
|
+
node = stack.pop()
|
|
540
|
+
|
|
541
|
+
if node.type == "call":
|
|
542
|
+
import_elem = self._extract_require_statement(node)
|
|
543
|
+
if import_elem:
|
|
544
|
+
imports.append(import_elem)
|
|
545
|
+
|
|
546
|
+
# Add children to stack
|
|
547
|
+
for child in reversed(node.children):
|
|
548
|
+
stack.append(child)
|
|
549
|
+
|
|
550
|
+
return imports
|
|
551
|
+
|
|
552
|
+
def _extract_require_statement(self, node: "tree_sitter.Node") -> Import | None:
|
|
553
|
+
"""
|
|
554
|
+
Extract require statement.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
node: Call node
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
Import element or None if not a require statement
|
|
561
|
+
"""
|
|
562
|
+
try:
|
|
563
|
+
# Check if this is a require call
|
|
564
|
+
method_node = node.child_by_field_name("method")
|
|
565
|
+
if not method_node:
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
method_name = self._get_node_text_optimized(method_node)
|
|
569
|
+
if method_name not in ("require", "require_relative", "load"):
|
|
570
|
+
return None
|
|
571
|
+
|
|
572
|
+
# Extract required module name
|
|
573
|
+
args_node = node.child_by_field_name("arguments")
|
|
574
|
+
if not args_node or not args_node.children:
|
|
575
|
+
return None
|
|
576
|
+
|
|
577
|
+
# Get first argument (the module name)
|
|
578
|
+
first_arg = args_node.children[0]
|
|
579
|
+
if first_arg.type == "string":
|
|
580
|
+
# Extract string content
|
|
581
|
+
import_name = self._get_node_text_optimized(first_arg).strip("\"'")
|
|
582
|
+
|
|
583
|
+
return Import(
|
|
584
|
+
name=import_name,
|
|
585
|
+
start_line=node.start_point[0] + 1,
|
|
586
|
+
end_line=node.end_point[0] + 1,
|
|
587
|
+
alias=None,
|
|
588
|
+
is_wildcard=False,
|
|
589
|
+
)
|
|
590
|
+
except Exception as e:
|
|
591
|
+
log_error(f"Error extracting require statement: {e}")
|
|
592
|
+
|
|
593
|
+
return None
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class RubyPlugin(LanguagePlugin):
|
|
597
|
+
"""
|
|
598
|
+
Ruby language plugin.
|
|
599
|
+
|
|
600
|
+
Provides Ruby-specific parsing and element extraction using tree-sitter-ruby.
|
|
601
|
+
Supports modern Ruby features including Ruby 3+ syntax.
|
|
602
|
+
"""
|
|
603
|
+
|
|
604
|
+
_language_instance: Optional["tree_sitter.Language"] = None
|
|
605
|
+
|
|
606
|
+
def get_language_name(self) -> str:
|
|
607
|
+
"""
|
|
608
|
+
Get the name of the language.
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
Language name string
|
|
612
|
+
"""
|
|
613
|
+
return "ruby"
|
|
614
|
+
|
|
615
|
+
def get_file_extensions(self) -> list[str]:
|
|
616
|
+
"""
|
|
617
|
+
Get supported file extensions.
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
List of file extensions
|
|
621
|
+
"""
|
|
622
|
+
return [".rb"]
|
|
623
|
+
|
|
624
|
+
def get_tree_sitter_language(self) -> "tree_sitter.Language":
|
|
625
|
+
"""
|
|
626
|
+
Get the tree-sitter language instance for Ruby.
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
tree-sitter Language instance
|
|
630
|
+
|
|
631
|
+
Raises:
|
|
632
|
+
ImportError: If tree-sitter-ruby is not installed
|
|
633
|
+
"""
|
|
634
|
+
if not TREE_SITTER_AVAILABLE:
|
|
635
|
+
raise ImportError(
|
|
636
|
+
"tree-sitter is not installed. Install it with: pip install tree-sitter"
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
if RubyPlugin._language_instance is None:
|
|
640
|
+
try:
|
|
641
|
+
import tree_sitter_ruby
|
|
642
|
+
|
|
643
|
+
RubyPlugin._language_instance = tree_sitter.Language(
|
|
644
|
+
tree_sitter_ruby.language()
|
|
645
|
+
)
|
|
646
|
+
except ImportError as e:
|
|
647
|
+
raise ImportError(
|
|
648
|
+
"tree-sitter-ruby is not installed. Install it with: pip install tree-sitter-ruby"
|
|
649
|
+
) from e
|
|
650
|
+
|
|
651
|
+
return RubyPlugin._language_instance
|
|
652
|
+
|
|
653
|
+
def create_extractor(self) -> ElementExtractor:
|
|
654
|
+
"""
|
|
655
|
+
Create a Ruby element extractor.
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
RubyElementExtractor instance
|
|
659
|
+
"""
|
|
660
|
+
return RubyElementExtractor()
|
|
661
|
+
|
|
662
|
+
async def analyze_file(
|
|
663
|
+
self, file_path: str, request: "AnalysisRequest"
|
|
664
|
+
) -> "AnalysisResult":
|
|
665
|
+
"""
|
|
666
|
+
Analyze a Ruby file.
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
file_path: Path to the Ruby file
|
|
670
|
+
request: Analysis request configuration
|
|
671
|
+
|
|
672
|
+
Returns:
|
|
673
|
+
AnalysisResult containing extracted elements
|
|
674
|
+
"""
|
|
675
|
+
from ..models import AnalysisResult
|
|
676
|
+
|
|
677
|
+
try:
|
|
678
|
+
# Load file content
|
|
679
|
+
content = await self._load_file_safe(file_path)
|
|
680
|
+
|
|
681
|
+
# Parse with tree-sitter
|
|
682
|
+
language = self.get_tree_sitter_language()
|
|
683
|
+
parser = tree_sitter.Parser(language)
|
|
684
|
+
tree = parser.parse(content.encode("utf-8"))
|
|
685
|
+
|
|
686
|
+
# Extract elements
|
|
687
|
+
extractor = self.create_extractor()
|
|
688
|
+
classes = extractor.extract_classes(tree, content)
|
|
689
|
+
functions = extractor.extract_functions(tree, content)
|
|
690
|
+
variables = extractor.extract_variables(tree, content)
|
|
691
|
+
imports = extractor.extract_imports(tree, content)
|
|
692
|
+
|
|
693
|
+
# Combine all elements
|
|
694
|
+
all_elements = classes + functions + variables + imports
|
|
695
|
+
|
|
696
|
+
return AnalysisResult(
|
|
697
|
+
language=self.get_language_name(),
|
|
698
|
+
file_path=file_path,
|
|
699
|
+
success=True,
|
|
700
|
+
elements=all_elements,
|
|
701
|
+
node_count=self._count_nodes(tree.root_node),
|
|
702
|
+
)
|
|
703
|
+
except Exception as e:
|
|
704
|
+
log_error(f"Error analyzing Ruby file {file_path}: {e}")
|
|
705
|
+
return AnalysisResult(
|
|
706
|
+
language=self.get_language_name(),
|
|
707
|
+
file_path=file_path,
|
|
708
|
+
success=False,
|
|
709
|
+
error_message=str(e),
|
|
710
|
+
elements=[],
|
|
711
|
+
node_count=0,
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
def _count_nodes(self, node: "tree_sitter.Node") -> int:
|
|
715
|
+
"""
|
|
716
|
+
Count total nodes in the AST.
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
node: Root node to count from
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
Total node count
|
|
723
|
+
"""
|
|
724
|
+
count = 1
|
|
725
|
+
for child in node.children:
|
|
726
|
+
count += self._count_nodes(child)
|
|
727
|
+
return count
|
|
728
|
+
|
|
729
|
+
async def _load_file_safe(self, file_path: str) -> str:
|
|
730
|
+
"""
|
|
731
|
+
Load file content with encoding detection.
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
file_path: Path to the file
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
File content as string
|
|
738
|
+
|
|
739
|
+
Raises:
|
|
740
|
+
IOError: If file cannot be read
|
|
741
|
+
"""
|
|
742
|
+
import chardet
|
|
743
|
+
|
|
744
|
+
try:
|
|
745
|
+
# Read file in binary mode
|
|
746
|
+
with open(file_path, "rb") as f:
|
|
747
|
+
raw_content = f.read()
|
|
748
|
+
|
|
749
|
+
# Detect encoding
|
|
750
|
+
detected = chardet.detect(raw_content)
|
|
751
|
+
encoding = detected.get("encoding", "utf-8")
|
|
752
|
+
|
|
753
|
+
# Decode with detected encoding
|
|
754
|
+
return raw_content.decode(encoding or "utf-8")
|
|
755
|
+
except Exception as e:
|
|
756
|
+
log_error(f"Error loading file {file_path}: {e}")
|
|
757
|
+
raise OSError(f"Failed to load file {file_path}: {e}") from e
|