roma-debug 0.1.0__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.
- roma_debug/__init__.py +3 -0
- roma_debug/config.py +79 -0
- roma_debug/core/__init__.py +5 -0
- roma_debug/core/engine.py +423 -0
- roma_debug/core/models.py +313 -0
- roma_debug/main.py +753 -0
- roma_debug/parsers/__init__.py +21 -0
- roma_debug/parsers/base.py +189 -0
- roma_debug/parsers/python_ast_parser.py +268 -0
- roma_debug/parsers/registry.py +196 -0
- roma_debug/parsers/traceback_patterns.py +314 -0
- roma_debug/parsers/treesitter_parser.py +598 -0
- roma_debug/prompts.py +153 -0
- roma_debug/server.py +247 -0
- roma_debug/tracing/__init__.py +28 -0
- roma_debug/tracing/call_chain.py +278 -0
- roma_debug/tracing/context_builder.py +672 -0
- roma_debug/tracing/dependency_graph.py +298 -0
- roma_debug/tracing/error_analyzer.py +399 -0
- roma_debug/tracing/import_resolver.py +315 -0
- roma_debug/tracing/project_scanner.py +569 -0
- roma_debug/utils/__init__.py +5 -0
- roma_debug/utils/context.py +422 -0
- roma_debug-0.1.0.dist-info/METADATA +34 -0
- roma_debug-0.1.0.dist-info/RECORD +36 -0
- roma_debug-0.1.0.dist-info/WHEEL +5 -0
- roma_debug-0.1.0.dist-info/entry_points.txt +2 -0
- roma_debug-0.1.0.dist-info/licenses/LICENSE +201 -0
- roma_debug-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_context.py +208 -0
- tests/test_engine.py +296 -0
- tests/test_parsers.py +534 -0
- tests/test_project_scanner.py +275 -0
- tests/test_traceback_patterns.py +222 -0
- tests/test_tracing.py +296 -0
tests/test_parsers.py
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"""Tests for the multi-language parser system."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from roma_debug.core.models import Language, Symbol, Import
|
|
5
|
+
from roma_debug.parsers.base import BaseParser
|
|
6
|
+
from roma_debug.parsers.registry import detect_language, get_parser, get_registry
|
|
7
|
+
from roma_debug.parsers.python_ast_parser import PythonAstParser
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestLanguageDetection:
|
|
11
|
+
"""Tests for language detection from file extensions."""
|
|
12
|
+
|
|
13
|
+
def test_python_extensions(self):
|
|
14
|
+
assert detect_language("main.py") == Language.PYTHON
|
|
15
|
+
assert detect_language("script.pyw") == Language.PYTHON
|
|
16
|
+
assert detect_language("types.pyi") == Language.PYTHON
|
|
17
|
+
|
|
18
|
+
def test_javascript_extensions(self):
|
|
19
|
+
assert detect_language("app.js") == Language.JAVASCRIPT
|
|
20
|
+
assert detect_language("module.mjs") == Language.JAVASCRIPT
|
|
21
|
+
assert detect_language("common.cjs") == Language.JAVASCRIPT
|
|
22
|
+
assert detect_language("component.jsx") == Language.JAVASCRIPT
|
|
23
|
+
|
|
24
|
+
def test_typescript_extensions(self):
|
|
25
|
+
assert detect_language("app.ts") == Language.TYPESCRIPT
|
|
26
|
+
assert detect_language("component.tsx") == Language.TYPESCRIPT
|
|
27
|
+
assert detect_language("module.mts") == Language.TYPESCRIPT
|
|
28
|
+
|
|
29
|
+
def test_go_extension(self):
|
|
30
|
+
assert detect_language("main.go") == Language.GO
|
|
31
|
+
|
|
32
|
+
def test_rust_extension(self):
|
|
33
|
+
assert detect_language("lib.rs") == Language.RUST
|
|
34
|
+
|
|
35
|
+
def test_java_extension(self):
|
|
36
|
+
assert detect_language("Main.java") == Language.JAVA
|
|
37
|
+
|
|
38
|
+
def test_unknown_extension(self):
|
|
39
|
+
assert detect_language("file.xyz") == Language.UNKNOWN
|
|
40
|
+
assert detect_language("noextension") == Language.UNKNOWN
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestPythonAstParser:
|
|
44
|
+
"""Tests for the Python AST parser."""
|
|
45
|
+
|
|
46
|
+
def test_parse_simple_function(self):
|
|
47
|
+
source = '''
|
|
48
|
+
def hello(name):
|
|
49
|
+
"""Say hello."""
|
|
50
|
+
return f"Hello, {name}!"
|
|
51
|
+
'''
|
|
52
|
+
parser = PythonAstParser()
|
|
53
|
+
assert parser.parse(source, "test.py") is True
|
|
54
|
+
assert parser.is_parsed
|
|
55
|
+
|
|
56
|
+
def test_find_enclosing_function(self):
|
|
57
|
+
source = '''
|
|
58
|
+
def outer():
|
|
59
|
+
x = 1
|
|
60
|
+
def inner():
|
|
61
|
+
y = 2
|
|
62
|
+
return y
|
|
63
|
+
return inner()
|
|
64
|
+
'''
|
|
65
|
+
parser = PythonAstParser()
|
|
66
|
+
parser.parse(source, "test.py")
|
|
67
|
+
|
|
68
|
+
# Line 5 is inside inner()
|
|
69
|
+
symbol = parser.find_enclosing_symbol(5)
|
|
70
|
+
assert symbol is not None
|
|
71
|
+
assert symbol.name == "inner"
|
|
72
|
+
assert symbol.kind == "function"
|
|
73
|
+
|
|
74
|
+
# Line 3 is inside outer()
|
|
75
|
+
symbol = parser.find_enclosing_symbol(3)
|
|
76
|
+
assert symbol is not None
|
|
77
|
+
assert symbol.name == "outer"
|
|
78
|
+
|
|
79
|
+
def test_find_enclosing_class(self):
|
|
80
|
+
source = '''
|
|
81
|
+
class MyClass:
|
|
82
|
+
"""A test class."""
|
|
83
|
+
|
|
84
|
+
def method(self):
|
|
85
|
+
return 42
|
|
86
|
+
'''
|
|
87
|
+
parser = PythonAstParser()
|
|
88
|
+
parser.parse(source, "test.py")
|
|
89
|
+
|
|
90
|
+
# Line 5 is inside method()
|
|
91
|
+
symbol = parser.find_enclosing_symbol(5)
|
|
92
|
+
assert symbol is not None
|
|
93
|
+
assert symbol.name == "method"
|
|
94
|
+
assert symbol.kind == "method"
|
|
95
|
+
|
|
96
|
+
# Class should be on line 2
|
|
97
|
+
symbol = parser.find_enclosing_symbol(2)
|
|
98
|
+
assert symbol is not None
|
|
99
|
+
assert symbol.name == "MyClass"
|
|
100
|
+
assert symbol.kind == "class"
|
|
101
|
+
|
|
102
|
+
def test_extract_imports(self):
|
|
103
|
+
source = '''
|
|
104
|
+
import os
|
|
105
|
+
import sys as system
|
|
106
|
+
from pathlib import Path
|
|
107
|
+
from typing import Optional, List
|
|
108
|
+
from . import local_module
|
|
109
|
+
from ..parent import something
|
|
110
|
+
'''
|
|
111
|
+
parser = PythonAstParser()
|
|
112
|
+
parser.parse(source, "test.py")
|
|
113
|
+
|
|
114
|
+
imports = parser.extract_imports()
|
|
115
|
+
assert len(imports) == 6
|
|
116
|
+
|
|
117
|
+
# Check import os
|
|
118
|
+
os_import = next((i for i in imports if i.module_name == "os"), None)
|
|
119
|
+
assert os_import is not None
|
|
120
|
+
assert os_import.alias is None
|
|
121
|
+
|
|
122
|
+
# Check import sys as system
|
|
123
|
+
sys_import = next((i for i in imports if i.module_name == "sys"), None)
|
|
124
|
+
assert sys_import is not None
|
|
125
|
+
assert sys_import.alias == "system"
|
|
126
|
+
|
|
127
|
+
# Check from pathlib import Path
|
|
128
|
+
pathlib_import = next((i for i in imports if i.module_name == "pathlib"), None)
|
|
129
|
+
assert pathlib_import is not None
|
|
130
|
+
assert "Path" in pathlib_import.imported_names
|
|
131
|
+
|
|
132
|
+
# Check relative import
|
|
133
|
+
relative_import = next((i for i in imports if i.is_relative and i.relative_level == 1), None)
|
|
134
|
+
assert relative_import is not None
|
|
135
|
+
|
|
136
|
+
def test_async_function(self):
|
|
137
|
+
source = '''
|
|
138
|
+
async def fetch_data(url):
|
|
139
|
+
response = await client.get(url)
|
|
140
|
+
return response.json()
|
|
141
|
+
'''
|
|
142
|
+
parser = PythonAstParser()
|
|
143
|
+
parser.parse(source, "test.py")
|
|
144
|
+
|
|
145
|
+
symbol = parser.find_enclosing_symbol(3)
|
|
146
|
+
assert symbol is not None
|
|
147
|
+
assert symbol.name == "fetch_data"
|
|
148
|
+
assert symbol.kind == "async_function"
|
|
149
|
+
|
|
150
|
+
def test_decorators(self):
|
|
151
|
+
source = '''
|
|
152
|
+
@staticmethod
|
|
153
|
+
@some_decorator
|
|
154
|
+
def decorated_function():
|
|
155
|
+
pass
|
|
156
|
+
'''
|
|
157
|
+
parser = PythonAstParser()
|
|
158
|
+
parser.parse(source, "test.py")
|
|
159
|
+
|
|
160
|
+
symbols = parser.find_all_symbols()
|
|
161
|
+
assert len(symbols) == 1
|
|
162
|
+
assert "staticmethod" in symbols[0].decorators
|
|
163
|
+
assert "some_decorator" in symbols[0].decorators
|
|
164
|
+
|
|
165
|
+
def test_syntax_error_returns_false(self):
|
|
166
|
+
source = '''
|
|
167
|
+
def broken(
|
|
168
|
+
# missing closing paren and body
|
|
169
|
+
'''
|
|
170
|
+
parser = PythonAstParser()
|
|
171
|
+
assert parser.parse(source, "test.py") is False
|
|
172
|
+
assert not parser.is_parsed
|
|
173
|
+
|
|
174
|
+
def test_format_snippet(self):
|
|
175
|
+
source = "line1\nline2\nline3\nline4\nline5"
|
|
176
|
+
parser = PythonAstParser()
|
|
177
|
+
parser.parse(source, "test.py")
|
|
178
|
+
|
|
179
|
+
snippet = parser.format_snippet(2, 4, highlight_line=3)
|
|
180
|
+
assert ">> " in snippet # Highlight marker
|
|
181
|
+
assert "line2" in snippet
|
|
182
|
+
assert "line3" in snippet
|
|
183
|
+
assert "line4" in snippet
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestParserRegistry:
|
|
187
|
+
"""Tests for the parser registry."""
|
|
188
|
+
|
|
189
|
+
def test_get_python_parser(self):
|
|
190
|
+
parser = get_parser(Language.PYTHON)
|
|
191
|
+
assert parser is not None
|
|
192
|
+
assert parser.language == Language.PYTHON
|
|
193
|
+
|
|
194
|
+
def test_get_parser_by_filepath(self):
|
|
195
|
+
parser = get_parser("test.py")
|
|
196
|
+
assert parser is not None
|
|
197
|
+
assert parser.language == Language.PYTHON
|
|
198
|
+
|
|
199
|
+
def test_registry_caches_parsers(self):
|
|
200
|
+
parser1 = get_parser(Language.PYTHON, create_new=False)
|
|
201
|
+
parser2 = get_parser(Language.PYTHON, create_new=False)
|
|
202
|
+
assert parser1 is parser2
|
|
203
|
+
|
|
204
|
+
def test_create_new_parser(self):
|
|
205
|
+
parser1 = get_parser(Language.PYTHON, create_new=True)
|
|
206
|
+
parser2 = get_parser(Language.PYTHON, create_new=True)
|
|
207
|
+
assert parser1 is not parser2
|
|
208
|
+
|
|
209
|
+
def test_registry_supports_language(self):
|
|
210
|
+
registry = get_registry()
|
|
211
|
+
assert registry.supports_language(Language.PYTHON)
|
|
212
|
+
|
|
213
|
+
def test_unsupported_language_returns_none(self):
|
|
214
|
+
parser = get_parser(Language.UNKNOWN)
|
|
215
|
+
assert parser is None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TestSymbol:
|
|
219
|
+
"""Tests for the Symbol class."""
|
|
220
|
+
|
|
221
|
+
def test_contains_line(self):
|
|
222
|
+
symbol = Symbol(
|
|
223
|
+
name="test",
|
|
224
|
+
kind="function",
|
|
225
|
+
start_line=10,
|
|
226
|
+
end_line=20,
|
|
227
|
+
)
|
|
228
|
+
assert symbol.contains_line(10)
|
|
229
|
+
assert symbol.contains_line(15)
|
|
230
|
+
assert symbol.contains_line(20)
|
|
231
|
+
assert not symbol.contains_line(9)
|
|
232
|
+
assert not symbol.contains_line(21)
|
|
233
|
+
|
|
234
|
+
def test_qualified_name_no_parent(self):
|
|
235
|
+
symbol = Symbol(name="func", kind="function", start_line=1, end_line=5)
|
|
236
|
+
assert symbol.qualified_name == "func"
|
|
237
|
+
|
|
238
|
+
def test_qualified_name_with_parent(self):
|
|
239
|
+
parent = Symbol(name="MyClass", kind="class", start_line=1, end_line=20)
|
|
240
|
+
child = Symbol(name="method", kind="method", start_line=5, end_line=10, parent=parent)
|
|
241
|
+
assert child.qualified_name == "MyClass.method"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class TestImport:
|
|
245
|
+
"""Tests for the Import class."""
|
|
246
|
+
|
|
247
|
+
def test_python_import_string(self):
|
|
248
|
+
imp = Import(
|
|
249
|
+
module_name="os",
|
|
250
|
+
language=Language.PYTHON,
|
|
251
|
+
)
|
|
252
|
+
assert imp.full_import_string == "import os"
|
|
253
|
+
|
|
254
|
+
def test_python_import_with_alias(self):
|
|
255
|
+
imp = Import(
|
|
256
|
+
module_name="numpy",
|
|
257
|
+
alias="np",
|
|
258
|
+
language=Language.PYTHON,
|
|
259
|
+
)
|
|
260
|
+
assert imp.full_import_string == "import numpy as np"
|
|
261
|
+
|
|
262
|
+
def test_python_from_import(self):
|
|
263
|
+
imp = Import(
|
|
264
|
+
module_name="pathlib",
|
|
265
|
+
imported_names=["Path", "PurePath"],
|
|
266
|
+
language=Language.PYTHON,
|
|
267
|
+
)
|
|
268
|
+
assert "from pathlib import" in imp.full_import_string
|
|
269
|
+
assert "Path" in imp.full_import_string
|
|
270
|
+
|
|
271
|
+
def test_python_relative_import(self):
|
|
272
|
+
imp = Import(
|
|
273
|
+
module_name="utils",
|
|
274
|
+
imported_names=["helper"],
|
|
275
|
+
is_relative=True,
|
|
276
|
+
relative_level=2,
|
|
277
|
+
language=Language.PYTHON,
|
|
278
|
+
)
|
|
279
|
+
assert "from ..utils import" in imp.full_import_string
|
|
280
|
+
|
|
281
|
+
def test_javascript_import_string(self):
|
|
282
|
+
imp = Import(
|
|
283
|
+
module_name="./utils",
|
|
284
|
+
alias="utils",
|
|
285
|
+
language=Language.JAVASCRIPT,
|
|
286
|
+
)
|
|
287
|
+
assert "import utils from" in imp.full_import_string
|
|
288
|
+
|
|
289
|
+
def test_javascript_named_import(self):
|
|
290
|
+
imp = Import(
|
|
291
|
+
module_name="lodash",
|
|
292
|
+
imported_names=["map", "filter"],
|
|
293
|
+
language=Language.JAVASCRIPT,
|
|
294
|
+
)
|
|
295
|
+
assert "import {" in imp.full_import_string
|
|
296
|
+
assert "map" in imp.full_import_string
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class TestTreeSitterParser:
|
|
300
|
+
"""Tests for the TreeSitter multi-language parser."""
|
|
301
|
+
|
|
302
|
+
def test_tree_sitter_available(self):
|
|
303
|
+
"""Verify tree-sitter is installed and available."""
|
|
304
|
+
from roma_debug.parsers.treesitter_parser import TREE_SITTER_AVAILABLE
|
|
305
|
+
assert TREE_SITTER_AVAILABLE, "tree-sitter should be available"
|
|
306
|
+
|
|
307
|
+
def test_javascript_parse_function(self):
|
|
308
|
+
"""Test parsing JavaScript function."""
|
|
309
|
+
source = '''
|
|
310
|
+
function greet(name) {
|
|
311
|
+
console.log("Hello, " + name);
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function multiply(x, y) {
|
|
316
|
+
return x * y;
|
|
317
|
+
}
|
|
318
|
+
'''
|
|
319
|
+
parser = get_parser(Language.JAVASCRIPT, create_new=True)
|
|
320
|
+
assert parser is not None
|
|
321
|
+
assert parser.parse(source, "test.js") is True
|
|
322
|
+
|
|
323
|
+
# Find symbols
|
|
324
|
+
symbols = parser.find_all_symbols()
|
|
325
|
+
names = [s.name for s in symbols]
|
|
326
|
+
assert "greet" in names
|
|
327
|
+
assert "multiply" in names
|
|
328
|
+
|
|
329
|
+
def test_javascript_find_enclosing_symbol(self):
|
|
330
|
+
"""Test finding enclosing function in JavaScript."""
|
|
331
|
+
source = '''function outer() {
|
|
332
|
+
let x = 1;
|
|
333
|
+
function inner() {
|
|
334
|
+
let y = 2;
|
|
335
|
+
return y;
|
|
336
|
+
}
|
|
337
|
+
return inner();
|
|
338
|
+
}'''
|
|
339
|
+
parser = get_parser(Language.JAVASCRIPT, create_new=True)
|
|
340
|
+
parser.parse(source, "test.js")
|
|
341
|
+
|
|
342
|
+
# Line 4 is inside inner()
|
|
343
|
+
symbol = parser.find_enclosing_symbol(4)
|
|
344
|
+
assert symbol is not None
|
|
345
|
+
assert symbol.name == "inner"
|
|
346
|
+
|
|
347
|
+
def test_javascript_extract_imports(self):
|
|
348
|
+
"""Test extracting JavaScript imports."""
|
|
349
|
+
source = '''
|
|
350
|
+
import React from 'react';
|
|
351
|
+
import { useState, useEffect } from 'react';
|
|
352
|
+
import * as utils from './utils';
|
|
353
|
+
const fs = require('fs');
|
|
354
|
+
'''
|
|
355
|
+
parser = get_parser(Language.JAVASCRIPT, create_new=True)
|
|
356
|
+
parser.parse(source, "test.js")
|
|
357
|
+
|
|
358
|
+
imports = parser.extract_imports()
|
|
359
|
+
module_names = [i.module_name for i in imports]
|
|
360
|
+
assert "react" in module_names or "'react'" in module_names
|
|
361
|
+
|
|
362
|
+
def test_go_parse_function(self):
|
|
363
|
+
"""Test parsing Go function."""
|
|
364
|
+
source = '''
|
|
365
|
+
package main
|
|
366
|
+
|
|
367
|
+
import "fmt"
|
|
368
|
+
|
|
369
|
+
func greet(name string) string {
|
|
370
|
+
return fmt.Sprintf("Hello, %s!", name)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
func main() {
|
|
374
|
+
msg := greet("World")
|
|
375
|
+
fmt.Println(msg)
|
|
376
|
+
}
|
|
377
|
+
'''
|
|
378
|
+
parser = get_parser(Language.GO, create_new=True)
|
|
379
|
+
assert parser is not None
|
|
380
|
+
assert parser.parse(source, "main.go") is True
|
|
381
|
+
|
|
382
|
+
symbols = parser.find_all_symbols()
|
|
383
|
+
names = [s.name for s in symbols]
|
|
384
|
+
assert "greet" in names
|
|
385
|
+
assert "main" in names
|
|
386
|
+
|
|
387
|
+
def test_go_find_enclosing_symbol(self):
|
|
388
|
+
"""Test finding enclosing function in Go."""
|
|
389
|
+
source = '''package main
|
|
390
|
+
|
|
391
|
+
func process(data []int) int {
|
|
392
|
+
sum := 0
|
|
393
|
+
for _, v := range data {
|
|
394
|
+
sum += v
|
|
395
|
+
}
|
|
396
|
+
return sum
|
|
397
|
+
}'''
|
|
398
|
+
parser = get_parser(Language.GO, create_new=True)
|
|
399
|
+
parser.parse(source, "main.go")
|
|
400
|
+
|
|
401
|
+
# Line 5 is inside for loop in process()
|
|
402
|
+
symbol = parser.find_enclosing_symbol(5)
|
|
403
|
+
assert symbol is not None
|
|
404
|
+
assert symbol.name == "process"
|
|
405
|
+
|
|
406
|
+
def test_go_extract_imports(self):
|
|
407
|
+
"""Test extracting Go imports."""
|
|
408
|
+
source = '''
|
|
409
|
+
package main
|
|
410
|
+
|
|
411
|
+
import (
|
|
412
|
+
"fmt"
|
|
413
|
+
"os"
|
|
414
|
+
"strings"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
func main() {}
|
|
418
|
+
'''
|
|
419
|
+
parser = get_parser(Language.GO, create_new=True)
|
|
420
|
+
parser.parse(source, "main.go")
|
|
421
|
+
|
|
422
|
+
imports = parser.extract_imports()
|
|
423
|
+
module_names = [i.module_name for i in imports]
|
|
424
|
+
assert any("fmt" in m for m in module_names)
|
|
425
|
+
|
|
426
|
+
def test_rust_parse_function(self):
|
|
427
|
+
"""Test parsing Rust function."""
|
|
428
|
+
source = '''
|
|
429
|
+
fn greet(name: &str) -> String {
|
|
430
|
+
format!("Hello, {}!", name)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
pub fn main() {
|
|
434
|
+
let msg = greet("World");
|
|
435
|
+
println!("{}", msg);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
struct Point {
|
|
439
|
+
x: i32,
|
|
440
|
+
y: i32,
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
impl Point {
|
|
444
|
+
fn new(x: i32, y: i32) -> Self {
|
|
445
|
+
Point { x, y }
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
'''
|
|
449
|
+
parser = get_parser(Language.RUST, create_new=True)
|
|
450
|
+
assert parser is not None
|
|
451
|
+
assert parser.parse(source, "main.rs") is True
|
|
452
|
+
|
|
453
|
+
symbols = parser.find_all_symbols()
|
|
454
|
+
names = [s.name for s in symbols]
|
|
455
|
+
assert "greet" in names
|
|
456
|
+
assert "main" in names
|
|
457
|
+
assert "Point" in names
|
|
458
|
+
|
|
459
|
+
def test_rust_find_enclosing_symbol(self):
|
|
460
|
+
"""Test finding enclosing function in Rust."""
|
|
461
|
+
source = '''fn process(data: Vec<i32>) -> i32 {
|
|
462
|
+
let mut sum = 0;
|
|
463
|
+
for v in data {
|
|
464
|
+
sum += v;
|
|
465
|
+
}
|
|
466
|
+
sum
|
|
467
|
+
}'''
|
|
468
|
+
parser = get_parser(Language.RUST, create_new=True)
|
|
469
|
+
parser.parse(source, "main.rs")
|
|
470
|
+
|
|
471
|
+
# Line 3 is inside for loop
|
|
472
|
+
symbol = parser.find_enclosing_symbol(3)
|
|
473
|
+
assert symbol is not None
|
|
474
|
+
assert symbol.name == "process"
|
|
475
|
+
|
|
476
|
+
def test_java_parse_class(self):
|
|
477
|
+
"""Test parsing Java class and methods."""
|
|
478
|
+
source = '''
|
|
479
|
+
public class Calculator {
|
|
480
|
+
private int value;
|
|
481
|
+
|
|
482
|
+
public Calculator() {
|
|
483
|
+
this.value = 0;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
public int add(int x) {
|
|
487
|
+
this.value += x;
|
|
488
|
+
return this.value;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
public static void main(String[] args) {
|
|
492
|
+
Calculator calc = new Calculator();
|
|
493
|
+
System.out.println(calc.add(5));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
'''
|
|
497
|
+
parser = get_parser(Language.JAVA, create_new=True)
|
|
498
|
+
assert parser is not None
|
|
499
|
+
assert parser.parse(source, "Calculator.java") is True
|
|
500
|
+
|
|
501
|
+
symbols = parser.find_all_symbols()
|
|
502
|
+
names = [s.name for s in symbols]
|
|
503
|
+
assert "Calculator" in names
|
|
504
|
+
assert "add" in names
|
|
505
|
+
assert "main" in names
|
|
506
|
+
|
|
507
|
+
def test_format_snippet_with_highlight(self):
|
|
508
|
+
"""Test snippet formatting with highlight line."""
|
|
509
|
+
source = "line1\nline2\nline3\nline4\nline5"
|
|
510
|
+
parser = get_parser(Language.JAVASCRIPT, create_new=True)
|
|
511
|
+
parser.parse(source, "test.js")
|
|
512
|
+
|
|
513
|
+
snippet = parser.format_snippet(2, 4, highlight_line=3)
|
|
514
|
+
assert "line2" in snippet
|
|
515
|
+
assert "line3" in snippet
|
|
516
|
+
assert "line4" in snippet
|
|
517
|
+
# Should have highlight marker on line 3
|
|
518
|
+
lines = snippet.split("\n")
|
|
519
|
+
line3 = [l for l in lines if "line3" in l][0]
|
|
520
|
+
assert ">>" in line3
|
|
521
|
+
|
|
522
|
+
def test_parser_fallback_gracefully(self):
|
|
523
|
+
"""Test that parsers handle invalid input gracefully."""
|
|
524
|
+
parser = get_parser(Language.JAVASCRIPT, create_new=True)
|
|
525
|
+
|
|
526
|
+
# Parse valid code first
|
|
527
|
+
valid = "function test() { return 1; }"
|
|
528
|
+
assert parser.parse(valid, "test.js") is True
|
|
529
|
+
|
|
530
|
+
# Now parse invalid/partial code - should still attempt parse
|
|
531
|
+
partial = "function incomplete("
|
|
532
|
+
result = parser.parse(partial, "broken.js")
|
|
533
|
+
# Tree-sitter is lenient, may still produce partial AST
|
|
534
|
+
# The important thing is it doesn't crash
|