Navegador

test: expand coverage to 97% across all parsers and context loader Add happy-path tests for all language parsers (_get_language, __init__, parse_file), _walk dispatch branches for Rust/Java/TypeScript, context loader iteration branches (callers, decorators, subclasses, references, explain, concepts), knowledge ingester domain-linking paths, and defensive continue branches in the ingestion orchestrator.

lmata 2026-03-23 00:17 trunk
Commit 7ae008029f20f95cac8db3109567e469c2cf7b1374684e95c7af6e67e33e6ea7
--- tests/test_context.py
+++ tests/test_context.py
@@ -280,5 +280,187 @@
280280
store = _mock_store(rows)
281281
loader = ContextLoader(store)
282282
bundle = loader.load_domain("auth")
283283
assert bundle.target.name == "auth"
284284
assert bundle.target.type == "Domain"
285
+
286
+
287
+# ── to_markdown with status and node docstrings ───────────────────────────────
288
+
289
+class TestContextBundleMarkdownBranches:
290
+ def test_markdown_includes_status(self):
291
+ target = ContextNode(type="Concept", name="JWT", status="active")
292
+ b = ContextBundle(target=target)
293
+ md = b.to_markdown()
294
+ assert "active" in md
295
+
296
+ def test_markdown_node_with_docstring(self):
297
+ target = ContextNode(type="File", name="app.py", file_path="app.py")
298
+ node = ContextNode(type="Function", name="foo", file_path="app.py",
299
+ docstring="Does something useful.")
300
+ b = ContextBundle(target=target, nodes=[node])
301
+ md = b.to_markdown()
302
+ assert "Does something useful." in md
303
+
304
+ def test_markdown_node_with_description_fallback(self):
305
+ target = ContextNode(type="Concept", name="JWT")
306
+ node = ContextNode(type="Rule", name="must_expire",
307
+ description="Tokens must expire.")
308
+ b = ContextBundle(target=target, nodes=[node])
309
+ md = b.to_markdown()
310
+ assert "Tokens must expire." in md
311
+
312
+
313
+# ── load_function with callers and decorators ─────────────────────────────────
314
+
315
+class TestContextLoaderFunctionBranches:
316
+ def test_load_function_with_callers(self):
317
+ call_count = [0]
318
+
319
+ def side_effect(query, params):
320
+ result = MagicMock()
321
+ call_count[0] += 1
322
+ if call_count[0] == 1:
323
+ result.result_set = [] # callees empty
324
+ elif call_count[0] == 2:
325
+ result.result_set = [["Function", "caller_fn", "src/x.py", 5]]
326
+ else:
327
+ result.result_set = [] # decorators empty
328
+ return result
329
+
330
+ store = MagicMock()
331
+ store.query.side_effect = side_effect
332
+ loader = ContextLoader(store)
333
+ bundle = loader.load_function("get_user", file_path="src/auth.py")
334
+ assert any(n.name == "caller_fn" for n in bundle.nodes)
335
+ assert any(e["type"] == "CALLS" for e in bundle.edges)
336
+
337
+ def test_load_function_with_decorators(self):
338
+ call_count = [0]
339
+
340
+ def side_effect(query, params):
341
+ result = MagicMock()
342
+ call_count[0] += 1
343
+ if call_count[0] <= 2:
344
+ result.result_set = [] # callees and callers empty
345
+ else:
346
+ result.result_set = [["login_required", "src/decorators.py"]]
347
+ return result
348
+
349
+ store = MagicMock()
350
+ store.query.side_effect = side_effect
351
+ loader = ContextLoader(store)
352
+ bundle = loader.load_function("my_view", file_path="src/views.py")
353
+ assert any(n.name == "login_required" for n in bundle.nodes)
354
+
355
+
356
+# ── load_class with subs and refs ─────────────────────────────────────────────
357
+
358
+class TestContextLoaderClassBranches:
359
+ def test_load_class_with_subclasses(self):
360
+ call_count = [0]
361
+
362
+ def side_effect(query, params):
363
+ result = MagicMock()
364
+ call_count[0] += 1
365
+ if call_count[0] == 1:
366
+ result.result_set = [] # parents empty
367
+ elif call_count[0] == 2:
368
+ result.result_set = [["ChildService", "src/child.py"]]
369
+ else:
370
+ result.result_set = [] # refs empty
371
+ return result
372
+
373
+ store = MagicMock()
374
+ store.query.side_effect = side_effect
375
+ loader = ContextLoader(store)
376
+ bundle = loader.load_class("BaseService")
377
+ assert any(n.name == "ChildService" for n in bundle.nodes)
378
+ assert any(e["type"] == "INHERITS" for e in bundle.edges)
379
+
380
+ def test_load_class_with_references(self):
381
+ call_count = [0]
382
+
383
+ def side_effect(query, params):
384
+ result = MagicMock()
385
+ call_count[0] += 1
386
+ if call_count[0] <= 2:
387
+ result.result_set = [] # parents and subs empty
388
+ else:
389
+ result.result_set = [["Function", "use_service", "src/x.py", 10]]
390
+ return result
391
+
392
+ store = MagicMock()
393
+ store.query.side_effect = side_effect
394
+ loader = ContextLoader(store)
395
+ bundle = loader.load_class("AuthService")
396
+ assert any(n.name == "use_service" for n in bundle.nodes)
397
+
398
+
399
+# ── explain with data ─────────────────────────────────────────────────────────
400
+
401
+class TestContextLoaderExplainBranches:
402
+ def test_explain_with_outbound_data(self):
403
+ call_count = [0]
404
+
405
+ def side_effect(query, params):
406
+ result = MagicMock()
407
+ call_count[0] += 1
408
+ if call_count[0] == 1:
409
+ result.result_set = [["CALLS", "Function", "helper", "src/utils.py"]]
410
+ else:
411
+ result.result_set = []
412
+ return result
413
+
414
+ store = MagicMock()
415
+ store.query.side_effect = side_effect
416
+ loader = ContextLoader(store)
417
+ bundle = loader.explain("main_fn")
418
+ assert any(n.name == "helper" for n in bundle.nodes)
419
+ assert any(e["type"] == "CALLS" for e in bundle.edges)
420
+
421
+ def test_explain_with_inbound_data(self):
422
+ call_count = [0]
423
+
424
+ def side_effect(query, params):
425
+ result = MagicMock()
426
+ call_count[0] += 1
427
+ if call_count[0] == 1:
428
+ result.result_set = []
429
+ else:
430
+ result.result_set = [["CALLS", "Function", "caller", "src/main.py"]]
431
+ return result
432
+
433
+ store = MagicMock()
434
+ store.query.side_effect = side_effect
435
+ loader = ContextLoader(store)
436
+ bundle = loader.explain("helper_fn")
437
+ assert any(n.name == "caller" for n in bundle.nodes)
438
+
439
+
440
+# ── load_concept with populated related nodes ─────────────────────────────────
441
+
442
+class TestContextLoaderConceptBranches:
443
+ def test_load_concept_with_related_concepts_rules_wiki_implements(self):
444
+ rows = [[
445
+ "JWT",
446
+ "Stateless token auth",
447
+ "active",
448
+ "auth",
449
+ ["OAuth"], # related concepts
450
+ ["Tokens must expire"], # rules
451
+ ["Auth Overview"], # wiki pages
452
+ ["validate_token"], # implementing code
453
+ ]]
454
+ store = _mock_store(rows)
455
+ loader = ContextLoader(store)
456
+ bundle = loader.load_concept("JWT")
457
+ names = {n.name for n in bundle.nodes}
458
+ assert "OAuth" in names
459
+ assert "Tokens must expire" in names
460
+ assert "Auth Overview" in names
461
+ assert "validate_token" in names
462
+ types = {e["type"] for e in bundle.edges}
463
+ assert "RELATED_TO" in types
464
+ assert "GOVERNS" in types
465
+ assert "DOCUMENTS" in types
466
+ assert "IMPLEMENTS" in types
285467
--- tests/test_context.py
+++ tests/test_context.py
@@ -280,5 +280,187 @@
280 store = _mock_store(rows)
281 loader = ContextLoader(store)
282 bundle = loader.load_domain("auth")
283 assert bundle.target.name == "auth"
284 assert bundle.target.type == "Domain"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
--- tests/test_context.py
+++ tests/test_context.py
@@ -280,5 +280,187 @@
280 store = _mock_store(rows)
281 loader = ContextLoader(store)
282 bundle = loader.load_domain("auth")
283 assert bundle.target.name == "auth"
284 assert bundle.target.type == "Domain"
285
286
287 # ── to_markdown with status and node docstrings ───────────────────────────────
288
289 class TestContextBundleMarkdownBranches:
290 def test_markdown_includes_status(self):
291 target = ContextNode(type="Concept", name="JWT", status="active")
292 b = ContextBundle(target=target)
293 md = b.to_markdown()
294 assert "active" in md
295
296 def test_markdown_node_with_docstring(self):
297 target = ContextNode(type="File", name="app.py", file_path="app.py")
298 node = ContextNode(type="Function", name="foo", file_path="app.py",
299 docstring="Does something useful.")
300 b = ContextBundle(target=target, nodes=[node])
301 md = b.to_markdown()
302 assert "Does something useful." in md
303
304 def test_markdown_node_with_description_fallback(self):
305 target = ContextNode(type="Concept", name="JWT")
306 node = ContextNode(type="Rule", name="must_expire",
307 description="Tokens must expire.")
308 b = ContextBundle(target=target, nodes=[node])
309 md = b.to_markdown()
310 assert "Tokens must expire." in md
311
312
313 # ── load_function with callers and decorators ─────────────────────────────────
314
315 class TestContextLoaderFunctionBranches:
316 def test_load_function_with_callers(self):
317 call_count = [0]
318
319 def side_effect(query, params):
320 result = MagicMock()
321 call_count[0] += 1
322 if call_count[0] == 1:
323 result.result_set = [] # callees empty
324 elif call_count[0] == 2:
325 result.result_set = [["Function", "caller_fn", "src/x.py", 5]]
326 else:
327 result.result_set = [] # decorators empty
328 return result
329
330 store = MagicMock()
331 store.query.side_effect = side_effect
332 loader = ContextLoader(store)
333 bundle = loader.load_function("get_user", file_path="src/auth.py")
334 assert any(n.name == "caller_fn" for n in bundle.nodes)
335 assert any(e["type"] == "CALLS" for e in bundle.edges)
336
337 def test_load_function_with_decorators(self):
338 call_count = [0]
339
340 def side_effect(query, params):
341 result = MagicMock()
342 call_count[0] += 1
343 if call_count[0] <= 2:
344 result.result_set = [] # callees and callers empty
345 else:
346 result.result_set = [["login_required", "src/decorators.py"]]
347 return result
348
349 store = MagicMock()
350 store.query.side_effect = side_effect
351 loader = ContextLoader(store)
352 bundle = loader.load_function("my_view", file_path="src/views.py")
353 assert any(n.name == "login_required" for n in bundle.nodes)
354
355
356 # ── load_class with subs and refs ─────────────────────────────────────────────
357
358 class TestContextLoaderClassBranches:
359 def test_load_class_with_subclasses(self):
360 call_count = [0]
361
362 def side_effect(query, params):
363 result = MagicMock()
364 call_count[0] += 1
365 if call_count[0] == 1:
366 result.result_set = [] # parents empty
367 elif call_count[0] == 2:
368 result.result_set = [["ChildService", "src/child.py"]]
369 else:
370 result.result_set = [] # refs empty
371 return result
372
373 store = MagicMock()
374 store.query.side_effect = side_effect
375 loader = ContextLoader(store)
376 bundle = loader.load_class("BaseService")
377 assert any(n.name == "ChildService" for n in bundle.nodes)
378 assert any(e["type"] == "INHERITS" for e in bundle.edges)
379
380 def test_load_class_with_references(self):
381 call_count = [0]
382
383 def side_effect(query, params):
384 result = MagicMock()
385 call_count[0] += 1
386 if call_count[0] <= 2:
387 result.result_set = [] # parents and subs empty
388 else:
389 result.result_set = [["Function", "use_service", "src/x.py", 10]]
390 return result
391
392 store = MagicMock()
393 store.query.side_effect = side_effect
394 loader = ContextLoader(store)
395 bundle = loader.load_class("AuthService")
396 assert any(n.name == "use_service" for n in bundle.nodes)
397
398
399 # ── explain with data ─────────────────────────────────────────────────────────
400
401 class TestContextLoaderExplainBranches:
402 def test_explain_with_outbound_data(self):
403 call_count = [0]
404
405 def side_effect(query, params):
406 result = MagicMock()
407 call_count[0] += 1
408 if call_count[0] == 1:
409 result.result_set = [["CALLS", "Function", "helper", "src/utils.py"]]
410 else:
411 result.result_set = []
412 return result
413
414 store = MagicMock()
415 store.query.side_effect = side_effect
416 loader = ContextLoader(store)
417 bundle = loader.explain("main_fn")
418 assert any(n.name == "helper" for n in bundle.nodes)
419 assert any(e["type"] == "CALLS" for e in bundle.edges)
420
421 def test_explain_with_inbound_data(self):
422 call_count = [0]
423
424 def side_effect(query, params):
425 result = MagicMock()
426 call_count[0] += 1
427 if call_count[0] == 1:
428 result.result_set = []
429 else:
430 result.result_set = [["CALLS", "Function", "caller", "src/main.py"]]
431 return result
432
433 store = MagicMock()
434 store.query.side_effect = side_effect
435 loader = ContextLoader(store)
436 bundle = loader.explain("helper_fn")
437 assert any(n.name == "caller" for n in bundle.nodes)
438
439
440 # ── load_concept with populated related nodes ─────────────────────────────────
441
442 class TestContextLoaderConceptBranches:
443 def test_load_concept_with_related_concepts_rules_wiki_implements(self):
444 rows = [[
445 "JWT",
446 "Stateless token auth",
447 "active",
448 "auth",
449 ["OAuth"], # related concepts
450 ["Tokens must expire"], # rules
451 ["Auth Overview"], # wiki pages
452 ["validate_token"], # implementing code
453 ]]
454 store = _mock_store(rows)
455 loader = ContextLoader(store)
456 bundle = loader.load_concept("JWT")
457 names = {n.name for n in bundle.nodes}
458 assert "OAuth" in names
459 assert "Tokens must expire" in names
460 assert "Auth Overview" in names
461 assert "validate_token" in names
462 types = {e["type"] for e in bundle.edges}
463 assert "RELATED_TO" in types
464 assert "GOVERNS" in types
465 assert "DOCUMENTS" in types
466 assert "IMPLEMENTS" in types
467
--- tests/test_go_parser.py
+++ tests/test_go_parser.py
@@ -231,5 +231,187 @@
231231
start_point=(0, 0), end_point=(0, 17))
232232
root = MockNode("source_file", children=[decl])
233233
stats = {"functions": 0, "classes": 0, "edges": 0}
234234
parser._walk(root, source, "main.go", store, stats)
235235
assert stats["classes"] == 1
236
+
237
+
238
+# ── _get_go_language happy path ───────────────────────────────────────────────
239
+
240
+class TestGoGetLanguageHappyPath:
241
+ def test_returns_language_object(self):
242
+ from navegador.ingestion.go import _get_go_language
243
+ mock_tsgo = MagicMock()
244
+ mock_ts = MagicMock()
245
+ with patch.dict("sys.modules", {
246
+ "tree_sitter_go": mock_tsgo,
247
+ "tree_sitter": mock_ts,
248
+ }):
249
+ result = _get_go_language()
250
+ assert result is mock_ts.Language.return_value
251
+
252
+
253
+# ── GoParser init and parse_file ─────────────────────────────────────────────
254
+
255
+class TestGoParserInit:
256
+ def test_init_creates_parser(self):
257
+ mock_tsgo = MagicMock()
258
+ mock_ts = MagicMock()
259
+ with patch.dict("sys.modules", {
260
+ "tree_sitter_go": mock_tsgo,
261
+ "tree_sitter": mock_ts,
262
+ }):
263
+ from navegador.ingestion.go import GoParser
264
+ parser = GoParser()
265
+ assert parser._parser is mock_ts.Parser.return_value
266
+
267
+ def test_parse_file_creates_file_node(self):
268
+ import tempfile
269
+ from pathlib import Path
270
+
271
+ from navegador.graph.schema import NodeLabel
272
+ parser = _make_parser()
273
+ store = _make_store()
274
+ mock_tree = MagicMock()
275
+ mock_tree.root_node.type = "source_file"
276
+ mock_tree.root_node.children = []
277
+ parser._parser.parse.return_value = mock_tree
278
+ with tempfile.NamedTemporaryFile(suffix=".go", delete=False) as f:
279
+ f.write(b"package main\n")
280
+ fpath = Path(f.name)
281
+ try:
282
+ parser.parse_file(fpath, fpath.parent, store)
283
+ store.create_node.assert_called_once()
284
+ assert store.create_node.call_args[0][0] == NodeLabel.File
285
+ assert store.create_node.call_args[0][1]["language"] == "go"
286
+ finally:
287
+ fpath.unlink()
288
+
289
+
290
+# ── _handle_method ────────────────────────────────────────────────────────────
291
+
292
+class TestGoHandleMethod:
293
+ def test_extracts_value_receiver(self):
294
+ parser = _make_parser()
295
+ store = _make_store()
296
+ source = b"Repo"
297
+ type_id = MockNode("type_identifier", start_byte=0, end_byte=4)
298
+ param_decl = MockNode("parameter_declaration", children=[type_id])
299
+ recv_list = MockNode("parameter_list", children=[param_decl])
300
+ name = _text_node(b"Save")
301
+ node = MockNode("method_declaration", start_point=(0, 0), end_point=(0, 30))
302
+ node.set_field("receiver", recv_list)
303
+ node.set_field("name", name)
304
+ stats = {"functions": 0, "classes": 0, "edges": 0}
305
+ parser._handle_method(node, source, "main.go", store, stats)
306
+ assert stats["functions"] == 1
307
+ label = store.create_node.call_args[0][0]
308
+ from navegador.graph.schema import NodeLabel
309
+ assert label == NodeLabel.Method
310
+
311
+ def test_extracts_pointer_receiver(self):
312
+ parser = _make_parser()
313
+ store = _make_store()
314
+ source = b"*Repo"
315
+ ptr_type = MockNode("pointer_type", start_byte=0, end_byte=5)
316
+ param_decl = MockNode("parameter_declaration", children=[ptr_type])
317
+ recv_list = MockNode("parameter_list", children=[param_decl])
318
+ name = _text_node(b"Delete")
319
+ node = MockNode("method_declaration", start_point=(0, 0), end_point=(0, 30))
320
+ node.set_field("receiver", recv_list)
321
+ node.set_field("name", name)
322
+ stats = {"functions": 0, "classes": 0, "edges": 0}
323
+ parser._handle_method(node, source, "main.go", store, stats)
324
+ # pointer receiver "*Repo" → lstrip("*") → "Repo"
325
+ assert stats["functions"] == 1
326
+
327
+ def test_no_receiver_field(self):
328
+ parser = _make_parser()
329
+ store = _make_store()
330
+ source = b"foo"
331
+ name = _text_node(b"foo")
332
+ node = MockNode("method_declaration", start_point=(0, 0), end_point=(0, 10))
333
+ node.set_field("name", name)
334
+ stats = {"functions": 0, "classes": 0, "edges": 0}
335
+ parser._handle_method(node, source, "main.go", store, stats)
336
+ # No receiver → treated as plain function
337
+ assert stats["functions"] == 1
338
+
339
+
340
+# ── _handle_import with import_spec_list ─────────────────────────────────────
341
+
342
+class TestGoHandleImportSpecList:
343
+ def test_handles_grouped_imports(self):
344
+ parser = _make_parser()
345
+ store = _make_store()
346
+ source = b'"fmt"'
347
+ path_node = MockNode("interpreted_string_literal", start_byte=0, end_byte=5)
348
+ spec1 = MockNode("import_spec")
349
+ spec1.set_field("path", path_node)
350
+ spec_list = MockNode("import_spec_list", children=[spec1])
351
+ import_node = MockNode("import_declaration",
352
+ children=[spec_list],
353
+ start_point=(0, 0))
354
+ stats = {"functions": 0, "classes": 0, "edges": 0}
355
+ parser._handle_import(import_node, source, "main.go", store, stats)
356
+ assert stats["edges"] == 1
357
+
358
+
359
+# ── _walk dispatch additions ──────────────────────────────────────────────────
360
+
361
+class TestGoWalkDispatchAdditional:
362
+ def test_walk_handles_method_declaration(self):
363
+ parser = _make_parser()
364
+ store = _make_store()
365
+ source = b"Repo"
366
+ type_id = MockNode("type_identifier", start_byte=0, end_byte=4)
367
+ param_decl = MockNode("parameter_declaration", children=[type_id])
368
+ recv_list = MockNode("parameter_list", children=[param_decl])
369
+ name = _text_node(b"Save")
370
+ method = MockNode("method_declaration", start_point=(0, 0), end_point=(0, 30))
371
+ method.set_field("receiver", recv_list)
372
+ method.set_field("name", name)
373
+ root = MockNode("source_file", children=[method])
374
+ stats = {"functions": 0, "classes": 0, "edges": 0}
375
+ parser._walk(root, source, "main.go", store, stats)
376
+ assert stats["functions"] == 1
377
+
378
+ def test_walk_handles_import_declaration(self):
379
+ parser = _make_parser()
380
+ store = _make_store()
381
+ source = b'"fmt"'
382
+ path_node = MockNode("interpreted_string_literal", start_byte=0, end_byte=5)
383
+ spec = MockNode("import_spec")
384
+ spec.set_field("path", path_node)
385
+ import_node = MockNode("import_declaration",
386
+ children=[spec],
387
+ start_point=(0, 0))
388
+ root = MockNode("source_file", children=[import_node])
389
+ stats = {"functions": 0, "classes": 0, "edges": 0}
390
+ parser._walk(root, source, "main.go", store, stats)
391
+ assert stats["edges"] == 1
392
+
393
+
394
+class TestGoHandleTypeContinueBranches:
395
+ def test_skips_non_type_spec_children(self):
396
+ parser = _make_parser()
397
+ store = _make_store()
398
+ source = b""
399
+ # Child that is not type_spec
400
+ comment_child = MockNode("comment")
401
+ decl = MockNode("type_declaration", children=[comment_child],
402
+ start_point=(0, 0), end_point=(0, 10))
403
+ stats = {"functions": 0, "classes": 0, "edges": 0}
404
+ parser._handle_type(decl, source, "main.go", store, stats)
405
+ assert stats["classes"] == 0
406
+
407
+ def test_skips_type_spec_without_name_or_type(self):
408
+ parser = _make_parser()
409
+ store = _make_store()
410
+ source = b""
411
+ # type_spec with no fields set
412
+ spec = MockNode("type_spec")
413
+ decl = MockNode("type_declaration", children=[spec],
414
+ start_point=(0, 0), end_point=(0, 10))
415
+ stats = {"functions": 0, "classes": 0, "edges": 0}
416
+ parser._handle_type(decl, source, "main.go", store, stats)
417
+ assert stats["classes"] == 0
236418
--- tests/test_go_parser.py
+++ tests/test_go_parser.py
@@ -231,5 +231,187 @@
231 start_point=(0, 0), end_point=(0, 17))
232 root = MockNode("source_file", children=[decl])
233 stats = {"functions": 0, "classes": 0, "edges": 0}
234 parser._walk(root, source, "main.go", store, stats)
235 assert stats["classes"] == 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
--- tests/test_go_parser.py
+++ tests/test_go_parser.py
@@ -231,5 +231,187 @@
231 start_point=(0, 0), end_point=(0, 17))
232 root = MockNode("source_file", children=[decl])
233 stats = {"functions": 0, "classes": 0, "edges": 0}
234 parser._walk(root, source, "main.go", store, stats)
235 assert stats["classes"] == 1
236
237
238 # ── _get_go_language happy path ───────────────────────────────────────────────
239
240 class TestGoGetLanguageHappyPath:
241 def test_returns_language_object(self):
242 from navegador.ingestion.go import _get_go_language
243 mock_tsgo = MagicMock()
244 mock_ts = MagicMock()
245 with patch.dict("sys.modules", {
246 "tree_sitter_go": mock_tsgo,
247 "tree_sitter": mock_ts,
248 }):
249 result = _get_go_language()
250 assert result is mock_ts.Language.return_value
251
252
253 # ── GoParser init and parse_file ─────────────────────────────────────────────
254
255 class TestGoParserInit:
256 def test_init_creates_parser(self):
257 mock_tsgo = MagicMock()
258 mock_ts = MagicMock()
259 with patch.dict("sys.modules", {
260 "tree_sitter_go": mock_tsgo,
261 "tree_sitter": mock_ts,
262 }):
263 from navegador.ingestion.go import GoParser
264 parser = GoParser()
265 assert parser._parser is mock_ts.Parser.return_value
266
267 def test_parse_file_creates_file_node(self):
268 import tempfile
269 from pathlib import Path
270
271 from navegador.graph.schema import NodeLabel
272 parser = _make_parser()
273 store = _make_store()
274 mock_tree = MagicMock()
275 mock_tree.root_node.type = "source_file"
276 mock_tree.root_node.children = []
277 parser._parser.parse.return_value = mock_tree
278 with tempfile.NamedTemporaryFile(suffix=".go", delete=False) as f:
279 f.write(b"package main\n")
280 fpath = Path(f.name)
281 try:
282 parser.parse_file(fpath, fpath.parent, store)
283 store.create_node.assert_called_once()
284 assert store.create_node.call_args[0][0] == NodeLabel.File
285 assert store.create_node.call_args[0][1]["language"] == "go"
286 finally:
287 fpath.unlink()
288
289
290 # ── _handle_method ────────────────────────────────────────────────────────────
291
292 class TestGoHandleMethod:
293 def test_extracts_value_receiver(self):
294 parser = _make_parser()
295 store = _make_store()
296 source = b"Repo"
297 type_id = MockNode("type_identifier", start_byte=0, end_byte=4)
298 param_decl = MockNode("parameter_declaration", children=[type_id])
299 recv_list = MockNode("parameter_list", children=[param_decl])
300 name = _text_node(b"Save")
301 node = MockNode("method_declaration", start_point=(0, 0), end_point=(0, 30))
302 node.set_field("receiver", recv_list)
303 node.set_field("name", name)
304 stats = {"functions": 0, "classes": 0, "edges": 0}
305 parser._handle_method(node, source, "main.go", store, stats)
306 assert stats["functions"] == 1
307 label = store.create_node.call_args[0][0]
308 from navegador.graph.schema import NodeLabel
309 assert label == NodeLabel.Method
310
311 def test_extracts_pointer_receiver(self):
312 parser = _make_parser()
313 store = _make_store()
314 source = b"*Repo"
315 ptr_type = MockNode("pointer_type", start_byte=0, end_byte=5)
316 param_decl = MockNode("parameter_declaration", children=[ptr_type])
317 recv_list = MockNode("parameter_list", children=[param_decl])
318 name = _text_node(b"Delete")
319 node = MockNode("method_declaration", start_point=(0, 0), end_point=(0, 30))
320 node.set_field("receiver", recv_list)
321 node.set_field("name", name)
322 stats = {"functions": 0, "classes": 0, "edges": 0}
323 parser._handle_method(node, source, "main.go", store, stats)
324 # pointer receiver "*Repo" → lstrip("*") → "Repo"
325 assert stats["functions"] == 1
326
327 def test_no_receiver_field(self):
328 parser = _make_parser()
329 store = _make_store()
330 source = b"foo"
331 name = _text_node(b"foo")
332 node = MockNode("method_declaration", start_point=(0, 0), end_point=(0, 10))
333 node.set_field("name", name)
334 stats = {"functions": 0, "classes": 0, "edges": 0}
335 parser._handle_method(node, source, "main.go", store, stats)
336 # No receiver → treated as plain function
337 assert stats["functions"] == 1
338
339
340 # ── _handle_import with import_spec_list ─────────────────────────────────────
341
342 class TestGoHandleImportSpecList:
343 def test_handles_grouped_imports(self):
344 parser = _make_parser()
345 store = _make_store()
346 source = b'"fmt"'
347 path_node = MockNode("interpreted_string_literal", start_byte=0, end_byte=5)
348 spec1 = MockNode("import_spec")
349 spec1.set_field("path", path_node)
350 spec_list = MockNode("import_spec_list", children=[spec1])
351 import_node = MockNode("import_declaration",
352 children=[spec_list],
353 start_point=(0, 0))
354 stats = {"functions": 0, "classes": 0, "edges": 0}
355 parser._handle_import(import_node, source, "main.go", store, stats)
356 assert stats["edges"] == 1
357
358
359 # ── _walk dispatch additions ──────────────────────────────────────────────────
360
361 class TestGoWalkDispatchAdditional:
362 def test_walk_handles_method_declaration(self):
363 parser = _make_parser()
364 store = _make_store()
365 source = b"Repo"
366 type_id = MockNode("type_identifier", start_byte=0, end_byte=4)
367 param_decl = MockNode("parameter_declaration", children=[type_id])
368 recv_list = MockNode("parameter_list", children=[param_decl])
369 name = _text_node(b"Save")
370 method = MockNode("method_declaration", start_point=(0, 0), end_point=(0, 30))
371 method.set_field("receiver", recv_list)
372 method.set_field("name", name)
373 root = MockNode("source_file", children=[method])
374 stats = {"functions": 0, "classes": 0, "edges": 0}
375 parser._walk(root, source, "main.go", store, stats)
376 assert stats["functions"] == 1
377
378 def test_walk_handles_import_declaration(self):
379 parser = _make_parser()
380 store = _make_store()
381 source = b'"fmt"'
382 path_node = MockNode("interpreted_string_literal", start_byte=0, end_byte=5)
383 spec = MockNode("import_spec")
384 spec.set_field("path", path_node)
385 import_node = MockNode("import_declaration",
386 children=[spec],
387 start_point=(0, 0))
388 root = MockNode("source_file", children=[import_node])
389 stats = {"functions": 0, "classes": 0, "edges": 0}
390 parser._walk(root, source, "main.go", store, stats)
391 assert stats["edges"] == 1
392
393
394 class TestGoHandleTypeContinueBranches:
395 def test_skips_non_type_spec_children(self):
396 parser = _make_parser()
397 store = _make_store()
398 source = b""
399 # Child that is not type_spec
400 comment_child = MockNode("comment")
401 decl = MockNode("type_declaration", children=[comment_child],
402 start_point=(0, 0), end_point=(0, 10))
403 stats = {"functions": 0, "classes": 0, "edges": 0}
404 parser._handle_type(decl, source, "main.go", store, stats)
405 assert stats["classes"] == 0
406
407 def test_skips_type_spec_without_name_or_type(self):
408 parser = _make_parser()
409 store = _make_store()
410 source = b""
411 # type_spec with no fields set
412 spec = MockNode("type_spec")
413 decl = MockNode("type_declaration", children=[spec],
414 start_point=(0, 0), end_point=(0, 10))
415 stats = {"functions": 0, "classes": 0, "edges": 0}
416 parser._handle_type(decl, source, "main.go", store, stats)
417 assert stats["classes"] == 0
418
--- tests/test_ingestion_code.py
+++ tests/test_ingestion_code.py
@@ -320,5 +320,41 @@
320320
"navegador.ingestion.java": MagicMock(JavaParser=mock_java_class)
321321
}):
322322
result = ingester._get_parser("java")
323323
assert result is mock_java_parser
324324
mock_java_class.assert_called_once_with()
325
+
326
+
327
+# ── defensive continue branch ─────────────────────────────────────────────────
328
+
329
+class TestIngesterContinueBranch:
330
+ def test_skips_file_when_language_not_in_map(self):
331
+ """
332
+ _iter_source_files filters to LANGUAGE_MAP extensions, but ingest()
333
+ has a defensive `if not language: continue`. Test it by patching
334
+ _iter_source_files to yield a .rb path.
335
+ """
336
+ import tempfile
337
+ from pathlib import Path
338
+ from unittest.mock import patch
339
+ store = _make_store()
340
+ ingester = RepoIngester(store)
341
+ with tempfile.TemporaryDirectory() as tmpdir:
342
+ rb_file = Path(tmpdir) / "script.rb"
343
+ rb_file.write_text("puts 'hello'")
344
+ with patch.object(ingester, "_iter_source_files", return_value=[rb_file]):
345
+ stats = ingester.ingest(tmpdir)
346
+ assert stats["files"] == 0
347
+
348
+
349
+# ── LanguageParser base class ─────────────────────────────────────────────────
350
+
351
+class TestLanguageParserBase:
352
+ def test_parse_file_raises_not_implemented(self):
353
+ from pathlib import Path
354
+
355
+ import pytest
356
+
357
+ from navegador.ingestion.parser import LanguageParser
358
+ lp = LanguageParser()
359
+ with pytest.raises(NotImplementedError):
360
+ lp.parse_file(Path("/tmp/x.py"), Path("/tmp"), MagicMock())
325361
--- tests/test_ingestion_code.py
+++ tests/test_ingestion_code.py
@@ -320,5 +320,41 @@
320 "navegador.ingestion.java": MagicMock(JavaParser=mock_java_class)
321 }):
322 result = ingester._get_parser("java")
323 assert result is mock_java_parser
324 mock_java_class.assert_called_once_with()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
--- tests/test_ingestion_code.py
+++ tests/test_ingestion_code.py
@@ -320,5 +320,41 @@
320 "navegador.ingestion.java": MagicMock(JavaParser=mock_java_class)
321 }):
322 result = ingester._get_parser("java")
323 assert result is mock_java_parser
324 mock_java_class.assert_called_once_with()
325
326
327 # ── defensive continue branch ─────────────────────────────────────────────────
328
329 class TestIngesterContinueBranch:
330 def test_skips_file_when_language_not_in_map(self):
331 """
332 _iter_source_files filters to LANGUAGE_MAP extensions, but ingest()
333 has a defensive `if not language: continue`. Test it by patching
334 _iter_source_files to yield a .rb path.
335 """
336 import tempfile
337 from pathlib import Path
338 from unittest.mock import patch
339 store = _make_store()
340 ingester = RepoIngester(store)
341 with tempfile.TemporaryDirectory() as tmpdir:
342 rb_file = Path(tmpdir) / "script.rb"
343 rb_file.write_text("puts 'hello'")
344 with patch.object(ingester, "_iter_source_files", return_value=[rb_file]):
345 stats = ingester.ingest(tmpdir)
346 assert stats["files"] == 0
347
348
349 # ── LanguageParser base class ─────────────────────────────────────────────────
350
351 class TestLanguageParserBase:
352 def test_parse_file_raises_not_implemented(self):
353 from pathlib import Path
354
355 import pytest
356
357 from navegador.ingestion.parser import LanguageParser
358 lp = LanguageParser()
359 with pytest.raises(NotImplementedError):
360 lp.parse_file(Path("/tmp/x.py"), Path("/tmp"), MagicMock())
361
--- tests/test_ingestion_knowledge.py
+++ tests/test_ingestion_knowledge.py
@@ -172,5 +172,29 @@
172172
def test_annotate_invalid_label_raises(self):
173173
store = _mock_store()
174174
k = KnowledgeIngester(store)
175175
with pytest.raises(ValueError):
176176
k.annotate_code("foo", "InvalidLabel", concept="Bar")
177
+
178
+
179
+class TestKnowledgeIngesterRuleWithDomain:
180
+ def test_add_rule_with_domain_creates_link(self):
181
+ from navegador.graph.schema import EdgeType
182
+ store = _mock_store()
183
+ k = KnowledgeIngester(store)
184
+ k.add_rule("Tokens must expire", domain="auth", severity="critical")
185
+ # create_node called twice: once for rule, once for domain
186
+ assert store.create_node.call_count == 2
187
+ # BELONGS_TO edge created
188
+ edge_calls = [c[0][2] for c in store.create_edge.call_args_list]
189
+ assert EdgeType.BELONGS_TO in edge_calls
190
+
191
+
192
+class TestKnowledgeIngesterDecisionWithDomain:
193
+ def test_add_decision_with_domain_creates_link(self):
194
+ from navegador.graph.schema import EdgeType
195
+ store = _mock_store()
196
+ k = KnowledgeIngester(store)
197
+ k.add_decision("Use PostgreSQL", domain="infra", status="accepted")
198
+ assert store.create_node.call_count == 2
199
+ edge_calls = [c[0][2] for c in store.create_edge.call_args_list]
200
+ assert EdgeType.BELONGS_TO in edge_calls
177201
--- tests/test_ingestion_knowledge.py
+++ tests/test_ingestion_knowledge.py
@@ -172,5 +172,29 @@
172 def test_annotate_invalid_label_raises(self):
173 store = _mock_store()
174 k = KnowledgeIngester(store)
175 with pytest.raises(ValueError):
176 k.annotate_code("foo", "InvalidLabel", concept="Bar")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
--- tests/test_ingestion_knowledge.py
+++ tests/test_ingestion_knowledge.py
@@ -172,5 +172,29 @@
172 def test_annotate_invalid_label_raises(self):
173 store = _mock_store()
174 k = KnowledgeIngester(store)
175 with pytest.raises(ValueError):
176 k.annotate_code("foo", "InvalidLabel", concept="Bar")
177
178
179 class TestKnowledgeIngesterRuleWithDomain:
180 def test_add_rule_with_domain_creates_link(self):
181 from navegador.graph.schema import EdgeType
182 store = _mock_store()
183 k = KnowledgeIngester(store)
184 k.add_rule("Tokens must expire", domain="auth", severity="critical")
185 # create_node called twice: once for rule, once for domain
186 assert store.create_node.call_count == 2
187 # BELONGS_TO edge created
188 edge_calls = [c[0][2] for c in store.create_edge.call_args_list]
189 assert EdgeType.BELONGS_TO in edge_calls
190
191
192 class TestKnowledgeIngesterDecisionWithDomain:
193 def test_add_decision_with_domain_creates_link(self):
194 from navegador.graph.schema import EdgeType
195 store = _mock_store()
196 k = KnowledgeIngester(store)
197 k.add_decision("Use PostgreSQL", domain="infra", status="accepted")
198 assert store.create_node.call_count == 2
199 edge_calls = [c[0][2] for c in store.create_edge.call_args_list]
200 assert EdgeType.BELONGS_TO in edge_calls
201
--- tests/test_java_parser.py
+++ tests/test_java_parser.py
@@ -251,5 +251,160 @@
251251
node = MockNode("method_declaration")
252252
node.set_field("body", MockNode("block"))
253253
stats = {"functions": 0, "classes": 0, "edges": 0}
254254
parser._extract_calls(node, b"", "X.java", "foo", store, stats)
255255
assert stats["edges"] == 0
256
+
257
+
258
+# ── _get_java_language happy path ─────────────────────────────────────────────
259
+
260
+class TestJavaGetLanguageHappyPath:
261
+ def test_returns_language_object(self):
262
+ from navegador.ingestion.java import _get_java_language
263
+ mock_tsjava = MagicMock()
264
+ mock_ts = MagicMock()
265
+ with patch.dict("sys.modules", {
266
+ "tree_sitter_java": mock_tsjava,
267
+ "tree_sitter": mock_ts,
268
+ }):
269
+ result = _get_java_language()
270
+ assert result is mock_ts.Language.return_value
271
+
272
+
273
+# ── JavaParser init and parse_file ───────────────────────────────────────────
274
+
275
+class TestJavaParserInit:
276
+ def test_init_creates_parser(self):
277
+ mock_tsjava = MagicMock()
278
+ mock_ts = MagicMock()
279
+ with patch.dict("sys.modules", {
280
+ "tree_sitter_java": mock_tsjava,
281
+ "tree_sitter": mock_ts,
282
+ }):
283
+ from navegador.ingestion.java import JavaParser
284
+ parser = JavaParser()
285
+ assert parser._parser is mock_ts.Parser.return_value
286
+
287
+ def test_parse_file_creates_file_node(self):
288
+ import tempfile
289
+ from pathlib import Path
290
+
291
+ from navegador.graph.schema import NodeLabel
292
+ parser = _make_parser()
293
+ store = _make_store()
294
+ mock_tree = MagicMock()
295
+ mock_tree.root_node.type = "program"
296
+ mock_tree.root_node.children = []
297
+ parser._parser.parse.return_value = mock_tree
298
+ with tempfile.NamedTemporaryFile(suffix=".java", delete=False) as f:
299
+ f.write(b"class Foo {}\n")
300
+ fpath = Path(f.name)
301
+ try:
302
+ parser.parse_file(fpath, fpath.parent, store)
303
+ store.create_node.assert_called_once()
304
+ assert store.create_node.call_args[0][0] == NodeLabel.File
305
+ assert store.create_node.call_args[0][1]["language"] == "java"
306
+ finally:
307
+ fpath.unlink()
308
+
309
+
310
+# ── _walk dispatch ────────────────────────────────────────────────────────────
311
+
312
+class TestJavaWalkDispatch:
313
+ def test_walk_handles_class_declaration(self):
314
+ parser = _make_parser()
315
+ store = _make_store()
316
+ source = b"Foo"
317
+ name = _text_node(b"Foo")
318
+ body = MockNode("class_body", children=[])
319
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 10))
320
+ node.set_field("name", name)
321
+ node.set_field("body", body)
322
+ root = MockNode("program", children=[node])
323
+ stats = {"functions": 0, "classes": 0, "edges": 0}
324
+ parser._walk(root, source, "Foo.java", store, stats, class_name=None)
325
+ assert stats["classes"] == 1
326
+
327
+ def test_walk_handles_interface_declaration(self):
328
+ parser = _make_parser()
329
+ store = _make_store()
330
+ source = b"Readable"
331
+ name = _text_node(b"Readable")
332
+ body = MockNode("interface_body", children=[])
333
+ node = MockNode("interface_declaration", start_point=(0, 0), end_point=(0, 20))
334
+ node.set_field("name", name)
335
+ node.set_field("body", body)
336
+ root = MockNode("program", children=[node])
337
+ stats = {"functions": 0, "classes": 0, "edges": 0}
338
+ parser._walk(root, source, "R.java", store, stats, class_name=None)
339
+ assert stats["classes"] == 1
340
+
341
+ def test_walk_handles_import_declaration(self):
342
+ parser = _make_parser()
343
+ store = _make_store()
344
+ source = b"import java.util.List;"
345
+ node = MockNode("import_declaration", start_byte=0, end_byte=22,
346
+ start_point=(0, 0))
347
+ root = MockNode("program", children=[node])
348
+ stats = {"functions": 0, "classes": 0, "edges": 0}
349
+ parser._walk(root, source, "Foo.java", store, stats, class_name=None)
350
+ assert stats["edges"] == 1
351
+
352
+ def test_walk_recurses_into_children(self):
353
+ parser = _make_parser()
354
+ store = _make_store()
355
+ source = b"import java.util.List;"
356
+ import_node = MockNode("import_declaration", start_byte=0, end_byte=22,
357
+ start_point=(0, 0))
358
+ wrapper = MockNode("block", children=[import_node])
359
+ root = MockNode("program", children=[wrapper])
360
+ stats = {"functions": 0, "classes": 0, "edges": 0}
361
+ parser._walk(root, source, "Foo.java", store, stats, class_name=None)
362
+ assert stats["edges"] == 1
363
+
364
+
365
+# ── _handle_class nested inner class ─────────────────────────────────────────
366
+
367
+class TestJavaHandleClassNested:
368
+ def test_ingests_nested_inner_class(self):
369
+ parser = _make_parser()
370
+ store = _make_store()
371
+ source = b"Inner"
372
+ outer_name = _text_node(b"Outer")
373
+ inner_name = _text_node(b"Inner")
374
+ inner_class = MockNode("class_declaration",
375
+ start_point=(1, 4), end_point=(3, 4))
376
+ inner_class.set_field("name", inner_name)
377
+ body = MockNode("class_body", children=[inner_class])
378
+ outer_class = MockNode("class_declaration",
379
+ start_point=(0, 0), end_point=(4, 0))
380
+ outer_class.set_field("name", outer_name)
381
+ outer_class.set_field("body", body)
382
+ stats = {"functions": 0, "classes": 0, "edges": 0}
383
+ parser._handle_class(outer_class, source, "Outer.java", store, stats)
384
+ # outer + inner class both registered
385
+ assert stats["classes"] == 2
386
+ assert stats["edges"] == 2 # CONTAINS(File→Outer) + CONTAINS(Outer→Inner)
387
+
388
+
389
+# ── _handle_interface with method body ───────────────────────────────────────
390
+
391
+class TestJavaHandleInterfaceWithMethods:
392
+ def test_walks_methods_in_interface_body(self):
393
+ parser = _make_parser()
394
+ store = _make_store()
395
+ source = b"read"
396
+ iface_name = _text_node(b"Readable")
397
+ method_name = _text_node(b"read")
398
+ method = MockNode("method_declaration",
399
+ start_point=(1, 4), end_point=(1, 20))
400
+ method.set_field("name", method_name)
401
+ body = MockNode("interface_body", children=[method])
402
+ iface = MockNode("interface_declaration",
403
+ start_point=(0, 0), end_point=(2, 0))
404
+ iface.set_field("name", iface_name)
405
+ iface.set_field("body", body)
406
+ stats = {"functions": 0, "classes": 0, "edges": 0}
407
+ parser._handle_interface(iface, source, "R.java", store, stats)
408
+ # interface node + method node
409
+ assert stats["classes"] == 1
410
+ assert stats["functions"] == 1
256411
--- tests/test_java_parser.py
+++ tests/test_java_parser.py
@@ -251,5 +251,160 @@
251 node = MockNode("method_declaration")
252 node.set_field("body", MockNode("block"))
253 stats = {"functions": 0, "classes": 0, "edges": 0}
254 parser._extract_calls(node, b"", "X.java", "foo", store, stats)
255 assert stats["edges"] == 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
--- tests/test_java_parser.py
+++ tests/test_java_parser.py
@@ -251,5 +251,160 @@
251 node = MockNode("method_declaration")
252 node.set_field("body", MockNode("block"))
253 stats = {"functions": 0, "classes": 0, "edges": 0}
254 parser._extract_calls(node, b"", "X.java", "foo", store, stats)
255 assert stats["edges"] == 0
256
257
258 # ── _get_java_language happy path ─────────────────────────────────────────────
259
260 class TestJavaGetLanguageHappyPath:
261 def test_returns_language_object(self):
262 from navegador.ingestion.java import _get_java_language
263 mock_tsjava = MagicMock()
264 mock_ts = MagicMock()
265 with patch.dict("sys.modules", {
266 "tree_sitter_java": mock_tsjava,
267 "tree_sitter": mock_ts,
268 }):
269 result = _get_java_language()
270 assert result is mock_ts.Language.return_value
271
272
273 # ── JavaParser init and parse_file ───────────────────────────────────────────
274
275 class TestJavaParserInit:
276 def test_init_creates_parser(self):
277 mock_tsjava = MagicMock()
278 mock_ts = MagicMock()
279 with patch.dict("sys.modules", {
280 "tree_sitter_java": mock_tsjava,
281 "tree_sitter": mock_ts,
282 }):
283 from navegador.ingestion.java import JavaParser
284 parser = JavaParser()
285 assert parser._parser is mock_ts.Parser.return_value
286
287 def test_parse_file_creates_file_node(self):
288 import tempfile
289 from pathlib import Path
290
291 from navegador.graph.schema import NodeLabel
292 parser = _make_parser()
293 store = _make_store()
294 mock_tree = MagicMock()
295 mock_tree.root_node.type = "program"
296 mock_tree.root_node.children = []
297 parser._parser.parse.return_value = mock_tree
298 with tempfile.NamedTemporaryFile(suffix=".java", delete=False) as f:
299 f.write(b"class Foo {}\n")
300 fpath = Path(f.name)
301 try:
302 parser.parse_file(fpath, fpath.parent, store)
303 store.create_node.assert_called_once()
304 assert store.create_node.call_args[0][0] == NodeLabel.File
305 assert store.create_node.call_args[0][1]["language"] == "java"
306 finally:
307 fpath.unlink()
308
309
310 # ── _walk dispatch ────────────────────────────────────────────────────────────
311
312 class TestJavaWalkDispatch:
313 def test_walk_handles_class_declaration(self):
314 parser = _make_parser()
315 store = _make_store()
316 source = b"Foo"
317 name = _text_node(b"Foo")
318 body = MockNode("class_body", children=[])
319 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 10))
320 node.set_field("name", name)
321 node.set_field("body", body)
322 root = MockNode("program", children=[node])
323 stats = {"functions": 0, "classes": 0, "edges": 0}
324 parser._walk(root, source, "Foo.java", store, stats, class_name=None)
325 assert stats["classes"] == 1
326
327 def test_walk_handles_interface_declaration(self):
328 parser = _make_parser()
329 store = _make_store()
330 source = b"Readable"
331 name = _text_node(b"Readable")
332 body = MockNode("interface_body", children=[])
333 node = MockNode("interface_declaration", start_point=(0, 0), end_point=(0, 20))
334 node.set_field("name", name)
335 node.set_field("body", body)
336 root = MockNode("program", children=[node])
337 stats = {"functions": 0, "classes": 0, "edges": 0}
338 parser._walk(root, source, "R.java", store, stats, class_name=None)
339 assert stats["classes"] == 1
340
341 def test_walk_handles_import_declaration(self):
342 parser = _make_parser()
343 store = _make_store()
344 source = b"import java.util.List;"
345 node = MockNode("import_declaration", start_byte=0, end_byte=22,
346 start_point=(0, 0))
347 root = MockNode("program", children=[node])
348 stats = {"functions": 0, "classes": 0, "edges": 0}
349 parser._walk(root, source, "Foo.java", store, stats, class_name=None)
350 assert stats["edges"] == 1
351
352 def test_walk_recurses_into_children(self):
353 parser = _make_parser()
354 store = _make_store()
355 source = b"import java.util.List;"
356 import_node = MockNode("import_declaration", start_byte=0, end_byte=22,
357 start_point=(0, 0))
358 wrapper = MockNode("block", children=[import_node])
359 root = MockNode("program", children=[wrapper])
360 stats = {"functions": 0, "classes": 0, "edges": 0}
361 parser._walk(root, source, "Foo.java", store, stats, class_name=None)
362 assert stats["edges"] == 1
363
364
365 # ── _handle_class nested inner class ─────────────────────────────────────────
366
367 class TestJavaHandleClassNested:
368 def test_ingests_nested_inner_class(self):
369 parser = _make_parser()
370 store = _make_store()
371 source = b"Inner"
372 outer_name = _text_node(b"Outer")
373 inner_name = _text_node(b"Inner")
374 inner_class = MockNode("class_declaration",
375 start_point=(1, 4), end_point=(3, 4))
376 inner_class.set_field("name", inner_name)
377 body = MockNode("class_body", children=[inner_class])
378 outer_class = MockNode("class_declaration",
379 start_point=(0, 0), end_point=(4, 0))
380 outer_class.set_field("name", outer_name)
381 outer_class.set_field("body", body)
382 stats = {"functions": 0, "classes": 0, "edges": 0}
383 parser._handle_class(outer_class, source, "Outer.java", store, stats)
384 # outer + inner class both registered
385 assert stats["classes"] == 2
386 assert stats["edges"] == 2 # CONTAINS(File→Outer) + CONTAINS(Outer→Inner)
387
388
389 # ── _handle_interface with method body ───────────────────────────────────────
390
391 class TestJavaHandleInterfaceWithMethods:
392 def test_walks_methods_in_interface_body(self):
393 parser = _make_parser()
394 store = _make_store()
395 source = b"read"
396 iface_name = _text_node(b"Readable")
397 method_name = _text_node(b"read")
398 method = MockNode("method_declaration",
399 start_point=(1, 4), end_point=(1, 20))
400 method.set_field("name", method_name)
401 body = MockNode("interface_body", children=[method])
402 iface = MockNode("interface_declaration",
403 start_point=(0, 0), end_point=(2, 0))
404 iface.set_field("name", iface_name)
405 iface.set_field("body", body)
406 stats = {"functions": 0, "classes": 0, "edges": 0}
407 parser._handle_interface(iface, source, "R.java", store, stats)
408 # interface node + method node
409 assert stats["classes"] == 1
410 assert stats["functions"] == 1
411
--- tests/test_python_parser.py
+++ tests/test_python_parser.py
@@ -265,5 +265,154 @@
265265
start_point=(0, 0), end_point=(0, 0))
266266
root = MockNode("module", children=[fn_node])
267267
stats = {"functions": 0, "classes": 0, "edges": 0}
268268
parser._walk(root, b"def my_fn(): pass", "app.py", store, stats, class_name=None)
269269
assert stats["functions"] == 1
270
+
271
+
272
+# ── _get_python_language happy path ──────────────────────────────────────────
273
+
274
+class TestGetPythonLanguageHappyPath:
275
+ def test_returns_language_object(self):
276
+ from navegador.ingestion.python import _get_python_language
277
+ mock_tspy = MagicMock()
278
+ mock_ts = MagicMock()
279
+ with patch.dict("sys.modules", {
280
+ "tree_sitter_python": mock_tspy,
281
+ "tree_sitter": mock_ts,
282
+ }):
283
+ result = _get_python_language()
284
+ assert result is mock_ts.Language.return_value
285
+
286
+
287
+# ── _get_parser ───────────────────────────────────────────────────────────────
288
+
289
+class TestGetParserHappyPath:
290
+ def test_returns_parser(self):
291
+ from navegador.ingestion.python import _get_parser
292
+ mock_tspy = MagicMock()
293
+ mock_ts = MagicMock()
294
+ with patch.dict("sys.modules", {
295
+ "tree_sitter_python": mock_tspy,
296
+ "tree_sitter": mock_ts,
297
+ }):
298
+ result = _get_parser()
299
+ assert result is mock_ts.Parser.return_value
300
+
301
+
302
+# ── parse_file ────────────────────────────────────────────────────────────────
303
+
304
+class TestPythonParseFile:
305
+ def _make_parser(self):
306
+ from navegador.ingestion.python import PythonParser
307
+ with patch("navegador.ingestion.python._get_parser") as mock_get:
308
+ mock_get.return_value = MagicMock()
309
+ parser = PythonParser()
310
+ return parser
311
+
312
+ def test_parse_file_creates_file_node(self):
313
+ import tempfile
314
+ from pathlib import Path
315
+ parser = self._make_parser()
316
+ store = MagicMock()
317
+ store.query.return_value = MagicMock(result_set=[])
318
+ mock_tree = MagicMock()
319
+ mock_tree.root_node.type = "module"
320
+ mock_tree.root_node.children = []
321
+ parser._parser.parse.return_value = mock_tree
322
+ with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
323
+ f.write(b"x = 1\n")
324
+ fpath = Path(f.name)
325
+ try:
326
+ stats = parser.parse_file(fpath, fpath.parent, store)
327
+ store.create_node.assert_called_once()
328
+ call = store.create_node.call_args[0]
329
+ from navegador.graph.schema import NodeLabel
330
+ assert call[0] == NodeLabel.File
331
+ assert call[1]["language"] == "python"
332
+ assert isinstance(stats, dict)
333
+ finally:
334
+ fpath.unlink()
335
+
336
+
337
+# ── _handle_import_from ───────────────────────────────────────────────────────
338
+
339
+class TestHandleImportFrom:
340
+ def _make_parser(self):
341
+ from navegador.ingestion.python import PythonParser
342
+ with patch("navegador.ingestion.python._get_parser") as mock_get:
343
+ mock_get.return_value = MagicMock()
344
+ parser = PythonParser()
345
+ return parser
346
+
347
+ def test_handle_import_from_with_member(self):
348
+ parser = self._make_parser()
349
+ store = MagicMock()
350
+ stats = {"functions": 0, "classes": 0, "edges": 0}
351
+ combined = b"os.pathjoin"
352
+ module_node3 = MockNode("dotted_name", start_byte=0, end_byte=7)
353
+ member_node3 = MockNode("import_from_member", start_byte=7, end_byte=11)
354
+ node3 = MockNode("import_from_statement",
355
+ children=[module_node3, member_node3],
356
+ start_point=(0, 0))
357
+ parser._handle_import_from(node3, combined, "app.py", store, stats)
358
+ store.create_node.assert_called_once()
359
+ store.create_edge.assert_called_once()
360
+ assert stats["edges"] == 1
361
+
362
+ def test_handle_import_from_no_member(self):
363
+ parser = self._make_parser()
364
+ store = MagicMock()
365
+ # No import_from_member children — nothing should be created
366
+ module_node = MockNode("dotted_name", start_byte=0, end_byte=7)
367
+ node = MockNode("import_from_statement",
368
+ children=[module_node],
369
+ start_point=(0, 0))
370
+ stats = {"functions": 0, "classes": 0, "edges": 0}
371
+ parser._handle_import_from(node, b"os.path", "app.py", store, stats)
372
+ store.create_node.assert_not_called()
373
+ assert stats["edges"] == 0
374
+
375
+ def test_walk_dispatches_import_from(self):
376
+ parser = self._make_parser()
377
+ store = MagicMock()
378
+ source = b"os.pathjoin"
379
+ module_node = MockNode("dotted_name", start_byte=0, end_byte=7)
380
+ member_node = MockNode("import_from_member", start_byte=7, end_byte=11)
381
+ import_from = MockNode("import_from_statement",
382
+ children=[module_node, member_node],
383
+ start_point=(0, 0))
384
+ root = MockNode("module", children=[import_from])
385
+ stats = {"functions": 0, "classes": 0, "edges": 0}
386
+ parser._walk(root, source, "app.py", store, stats, class_name=None)
387
+ assert stats["edges"] == 1
388
+
389
+
390
+# ── _handle_class with body ───────────────────────────────────────────────────
391
+
392
+class TestHandleClassWithBody:
393
+ def _make_parser(self):
394
+ from navegador.ingestion.python import PythonParser
395
+ with patch("navegador.ingestion.python._get_parser") as mock_get:
396
+ mock_get.return_value = MagicMock()
397
+ parser = PythonParser()
398
+ return parser
399
+
400
+ def test_handle_class_with_method_in_body(self):
401
+ parser = self._make_parser()
402
+ store = MagicMock()
403
+ source = b"method"
404
+ name_node = MockNode("identifier", start_byte=0, end_byte=5)
405
+ # Method inside the class body
406
+ method_name = MockNode("identifier", start_byte=0, end_byte=6)
407
+ fn_node = MockNode("function_definition",
408
+ children=[method_name],
409
+ start_point=(1, 4), end_point=(1, 20))
410
+ body = MockNode("block", children=[fn_node])
411
+ class_node = MockNode("class_definition",
412
+ children=[name_node, body],
413
+ start_point=(0, 0), end_point=(2, 0))
414
+ stats = {"functions": 0, "classes": 0, "edges": 0}
415
+ parser._handle_class(class_node, source, "app.py", store, stats)
416
+ # class node + method node both created
417
+ assert stats["classes"] == 1
418
+ assert stats["functions"] == 1
270419
--- tests/test_python_parser.py
+++ tests/test_python_parser.py
@@ -265,5 +265,154 @@
265 start_point=(0, 0), end_point=(0, 0))
266 root = MockNode("module", children=[fn_node])
267 stats = {"functions": 0, "classes": 0, "edges": 0}
268 parser._walk(root, b"def my_fn(): pass", "app.py", store, stats, class_name=None)
269 assert stats["functions"] == 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
--- tests/test_python_parser.py
+++ tests/test_python_parser.py
@@ -265,5 +265,154 @@
265 start_point=(0, 0), end_point=(0, 0))
266 root = MockNode("module", children=[fn_node])
267 stats = {"functions": 0, "classes": 0, "edges": 0}
268 parser._walk(root, b"def my_fn(): pass", "app.py", store, stats, class_name=None)
269 assert stats["functions"] == 1
270
271
272 # ── _get_python_language happy path ──────────────────────────────────────────
273
274 class TestGetPythonLanguageHappyPath:
275 def test_returns_language_object(self):
276 from navegador.ingestion.python import _get_python_language
277 mock_tspy = MagicMock()
278 mock_ts = MagicMock()
279 with patch.dict("sys.modules", {
280 "tree_sitter_python": mock_tspy,
281 "tree_sitter": mock_ts,
282 }):
283 result = _get_python_language()
284 assert result is mock_ts.Language.return_value
285
286
287 # ── _get_parser ───────────────────────────────────────────────────────────────
288
289 class TestGetParserHappyPath:
290 def test_returns_parser(self):
291 from navegador.ingestion.python import _get_parser
292 mock_tspy = MagicMock()
293 mock_ts = MagicMock()
294 with patch.dict("sys.modules", {
295 "tree_sitter_python": mock_tspy,
296 "tree_sitter": mock_ts,
297 }):
298 result = _get_parser()
299 assert result is mock_ts.Parser.return_value
300
301
302 # ── parse_file ────────────────────────────────────────────────────────────────
303
304 class TestPythonParseFile:
305 def _make_parser(self):
306 from navegador.ingestion.python import PythonParser
307 with patch("navegador.ingestion.python._get_parser") as mock_get:
308 mock_get.return_value = MagicMock()
309 parser = PythonParser()
310 return parser
311
312 def test_parse_file_creates_file_node(self):
313 import tempfile
314 from pathlib import Path
315 parser = self._make_parser()
316 store = MagicMock()
317 store.query.return_value = MagicMock(result_set=[])
318 mock_tree = MagicMock()
319 mock_tree.root_node.type = "module"
320 mock_tree.root_node.children = []
321 parser._parser.parse.return_value = mock_tree
322 with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
323 f.write(b"x = 1\n")
324 fpath = Path(f.name)
325 try:
326 stats = parser.parse_file(fpath, fpath.parent, store)
327 store.create_node.assert_called_once()
328 call = store.create_node.call_args[0]
329 from navegador.graph.schema import NodeLabel
330 assert call[0] == NodeLabel.File
331 assert call[1]["language"] == "python"
332 assert isinstance(stats, dict)
333 finally:
334 fpath.unlink()
335
336
337 # ── _handle_import_from ───────────────────────────────────────────────────────
338
339 class TestHandleImportFrom:
340 def _make_parser(self):
341 from navegador.ingestion.python import PythonParser
342 with patch("navegador.ingestion.python._get_parser") as mock_get:
343 mock_get.return_value = MagicMock()
344 parser = PythonParser()
345 return parser
346
347 def test_handle_import_from_with_member(self):
348 parser = self._make_parser()
349 store = MagicMock()
350 stats = {"functions": 0, "classes": 0, "edges": 0}
351 combined = b"os.pathjoin"
352 module_node3 = MockNode("dotted_name", start_byte=0, end_byte=7)
353 member_node3 = MockNode("import_from_member", start_byte=7, end_byte=11)
354 node3 = MockNode("import_from_statement",
355 children=[module_node3, member_node3],
356 start_point=(0, 0))
357 parser._handle_import_from(node3, combined, "app.py", store, stats)
358 store.create_node.assert_called_once()
359 store.create_edge.assert_called_once()
360 assert stats["edges"] == 1
361
362 def test_handle_import_from_no_member(self):
363 parser = self._make_parser()
364 store = MagicMock()
365 # No import_from_member children — nothing should be created
366 module_node = MockNode("dotted_name", start_byte=0, end_byte=7)
367 node = MockNode("import_from_statement",
368 children=[module_node],
369 start_point=(0, 0))
370 stats = {"functions": 0, "classes": 0, "edges": 0}
371 parser._handle_import_from(node, b"os.path", "app.py", store, stats)
372 store.create_node.assert_not_called()
373 assert stats["edges"] == 0
374
375 def test_walk_dispatches_import_from(self):
376 parser = self._make_parser()
377 store = MagicMock()
378 source = b"os.pathjoin"
379 module_node = MockNode("dotted_name", start_byte=0, end_byte=7)
380 member_node = MockNode("import_from_member", start_byte=7, end_byte=11)
381 import_from = MockNode("import_from_statement",
382 children=[module_node, member_node],
383 start_point=(0, 0))
384 root = MockNode("module", children=[import_from])
385 stats = {"functions": 0, "classes": 0, "edges": 0}
386 parser._walk(root, source, "app.py", store, stats, class_name=None)
387 assert stats["edges"] == 1
388
389
390 # ── _handle_class with body ───────────────────────────────────────────────────
391
392 class TestHandleClassWithBody:
393 def _make_parser(self):
394 from navegador.ingestion.python import PythonParser
395 with patch("navegador.ingestion.python._get_parser") as mock_get:
396 mock_get.return_value = MagicMock()
397 parser = PythonParser()
398 return parser
399
400 def test_handle_class_with_method_in_body(self):
401 parser = self._make_parser()
402 store = MagicMock()
403 source = b"method"
404 name_node = MockNode("identifier", start_byte=0, end_byte=5)
405 # Method inside the class body
406 method_name = MockNode("identifier", start_byte=0, end_byte=6)
407 fn_node = MockNode("function_definition",
408 children=[method_name],
409 start_point=(1, 4), end_point=(1, 20))
410 body = MockNode("block", children=[fn_node])
411 class_node = MockNode("class_definition",
412 children=[name_node, body],
413 start_point=(0, 0), end_point=(2, 0))
414 stats = {"functions": 0, "classes": 0, "edges": 0}
415 parser._handle_class(class_node, source, "app.py", store, stats)
416 # class node + method node both created
417 assert stats["classes"] == 1
418 assert stats["functions"] == 1
419
--- tests/test_rust_parser.py
+++ tests/test_rust_parser.py
@@ -253,5 +253,124 @@
253253
parser._extract_calls(fn_node, source, "lib.rs", "foo",
254254
NodeLabel.Function, store, stats)
255255
# "Repo::save" → callee = "save"
256256
edge_call = store.create_edge.call_args[0]
257257
assert edge_call[4]["name"] == "save"
258
+
259
+
260
+# ── _get_rust_language happy path ─────────────────────────────────────────────
261
+
262
+class TestRustGetLanguageHappyPath:
263
+ def test_returns_language_object(self):
264
+ from navegador.ingestion.rust import _get_rust_language
265
+ mock_tsrust = MagicMock()
266
+ mock_ts = MagicMock()
267
+ with patch.dict("sys.modules", {
268
+ "tree_sitter_rust": mock_tsrust,
269
+ "tree_sitter": mock_ts,
270
+ }):
271
+ result = _get_rust_language()
272
+ assert result is mock_ts.Language.return_value
273
+
274
+
275
+# ── RustParser init and parse_file ───────────────────────────────────────────
276
+
277
+class TestRustParserInit:
278
+ def test_init_creates_parser(self):
279
+ mock_tsrust = MagicMock()
280
+ mock_ts = MagicMock()
281
+ with patch.dict("sys.modules", {
282
+ "tree_sitter_rust": mock_tsrust,
283
+ "tree_sitter": mock_ts,
284
+ }):
285
+ from navegador.ingestion.rust import RustParser
286
+ parser = RustParser()
287
+ assert parser._parser is mock_ts.Parser.return_value
288
+
289
+ def test_parse_file_creates_file_node(self):
290
+ import tempfile
291
+ from pathlib import Path
292
+
293
+ from navegador.graph.schema import NodeLabel
294
+ parser = _make_parser()
295
+ store = _make_store()
296
+ mock_tree = MagicMock()
297
+ mock_tree.root_node.type = "source_file"
298
+ mock_tree.root_node.children = []
299
+ parser._parser.parse.return_value = mock_tree
300
+ with tempfile.NamedTemporaryFile(suffix=".rs", delete=False) as f:
301
+ f.write(b"fn main() {}\n")
302
+ fpath = Path(f.name)
303
+ try:
304
+ parser.parse_file(fpath, fpath.parent, store)
305
+ store.create_node.assert_called_once()
306
+ assert store.create_node.call_args[0][0] == NodeLabel.File
307
+ assert store.create_node.call_args[0][1]["language"] == "rust"
308
+ finally:
309
+ fpath.unlink()
310
+
311
+
312
+# ── _walk dispatch ────────────────────────────────────────────────────────────
313
+
314
+class TestRustWalkDispatch:
315
+ def test_walk_handles_function_item(self):
316
+ parser = _make_parser()
317
+ store = _make_store()
318
+ source = b"foo"
319
+ name = _text_node(b"foo")
320
+ fn_node = MockNode("function_item", start_point=(0, 0), end_point=(0, 10))
321
+ fn_node.set_field("name", name)
322
+ root = MockNode("source_file", children=[fn_node])
323
+ stats = {"functions": 0, "classes": 0, "edges": 0}
324
+ parser._walk(root, source, "lib.rs", store, stats, impl_type=None)
325
+ assert stats["functions"] == 1
326
+
327
+ def test_walk_handles_impl_item(self):
328
+ parser = _make_parser()
329
+ store = _make_store()
330
+ source = b"MyStruct"
331
+ type_node = MockNode("type_identifier", start_byte=0, end_byte=8)
332
+ body = MockNode("declaration_list", children=[])
333
+ impl_node = MockNode("impl_item")
334
+ impl_node.set_field("type", type_node)
335
+ impl_node.set_field("body", body)
336
+ root = MockNode("source_file", children=[impl_node])
337
+ stats = {"functions": 0, "classes": 0, "edges": 0}
338
+ parser._walk(root, source, "lib.rs", store, stats, impl_type=None)
339
+ # No functions in body, just verifies dispatch doesn't crash
340
+ assert stats["functions"] == 0
341
+
342
+ def test_walk_handles_struct_item(self):
343
+ parser = _make_parser()
344
+ store = _make_store()
345
+ source = b"Foo"
346
+ name = _text_node(b"Foo", "type_identifier")
347
+ node = MockNode("struct_item", start_point=(0, 0), end_point=(0, 10))
348
+ node.set_field("name", name)
349
+ _parent = MockNode("source_file", children=[node])
350
+ root = MockNode("source_file", children=[node])
351
+ stats = {"functions": 0, "classes": 0, "edges": 0}
352
+ parser._walk(root, source, "lib.rs", store, stats, impl_type=None)
353
+ assert stats["classes"] == 1
354
+
355
+ def test_walk_handles_use_declaration(self):
356
+ parser = _make_parser()
357
+ store = _make_store()
358
+ source = b"use std::io;"
359
+ use_node = MockNode("use_declaration", start_byte=0, end_byte=12)
360
+ root = MockNode("source_file", children=[use_node])
361
+ stats = {"functions": 0, "classes": 0, "edges": 0}
362
+ parser._walk(root, source, "lib.rs", store, stats, impl_type=None)
363
+ assert stats["edges"] == 1
364
+
365
+ def test_walk_recurses_into_unknown_nodes(self):
366
+ parser = _make_parser()
367
+ store = _make_store()
368
+ source = b"foo"
369
+ name = _text_node(b"foo")
370
+ fn_node = MockNode("function_item", start_point=(0, 0), end_point=(0, 10))
371
+ fn_node.set_field("name", name)
372
+ wrapper = MockNode("mod_item", children=[fn_node])
373
+ root = MockNode("source_file", children=[wrapper])
374
+ stats = {"functions": 0, "classes": 0, "edges": 0}
375
+ parser._walk(root, source, "lib.rs", store, stats, impl_type=None)
376
+ assert stats["functions"] == 1
258377
--- tests/test_rust_parser.py
+++ tests/test_rust_parser.py
@@ -253,5 +253,124 @@
253 parser._extract_calls(fn_node, source, "lib.rs", "foo",
254 NodeLabel.Function, store, stats)
255 # "Repo::save" → callee = "save"
256 edge_call = store.create_edge.call_args[0]
257 assert edge_call[4]["name"] == "save"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
--- tests/test_rust_parser.py
+++ tests/test_rust_parser.py
@@ -253,5 +253,124 @@
253 parser._extract_calls(fn_node, source, "lib.rs", "foo",
254 NodeLabel.Function, store, stats)
255 # "Repo::save" → callee = "save"
256 edge_call = store.create_edge.call_args[0]
257 assert edge_call[4]["name"] == "save"
258
259
260 # ── _get_rust_language happy path ─────────────────────────────────────────────
261
262 class TestRustGetLanguageHappyPath:
263 def test_returns_language_object(self):
264 from navegador.ingestion.rust import _get_rust_language
265 mock_tsrust = MagicMock()
266 mock_ts = MagicMock()
267 with patch.dict("sys.modules", {
268 "tree_sitter_rust": mock_tsrust,
269 "tree_sitter": mock_ts,
270 }):
271 result = _get_rust_language()
272 assert result is mock_ts.Language.return_value
273
274
275 # ── RustParser init and parse_file ───────────────────────────────────────────
276
277 class TestRustParserInit:
278 def test_init_creates_parser(self):
279 mock_tsrust = MagicMock()
280 mock_ts = MagicMock()
281 with patch.dict("sys.modules", {
282 "tree_sitter_rust": mock_tsrust,
283 "tree_sitter": mock_ts,
284 }):
285 from navegador.ingestion.rust import RustParser
286 parser = RustParser()
287 assert parser._parser is mock_ts.Parser.return_value
288
289 def test_parse_file_creates_file_node(self):
290 import tempfile
291 from pathlib import Path
292
293 from navegador.graph.schema import NodeLabel
294 parser = _make_parser()
295 store = _make_store()
296 mock_tree = MagicMock()
297 mock_tree.root_node.type = "source_file"
298 mock_tree.root_node.children = []
299 parser._parser.parse.return_value = mock_tree
300 with tempfile.NamedTemporaryFile(suffix=".rs", delete=False) as f:
301 f.write(b"fn main() {}\n")
302 fpath = Path(f.name)
303 try:
304 parser.parse_file(fpath, fpath.parent, store)
305 store.create_node.assert_called_once()
306 assert store.create_node.call_args[0][0] == NodeLabel.File
307 assert store.create_node.call_args[0][1]["language"] == "rust"
308 finally:
309 fpath.unlink()
310
311
312 # ── _walk dispatch ────────────────────────────────────────────────────────────
313
314 class TestRustWalkDispatch:
315 def test_walk_handles_function_item(self):
316 parser = _make_parser()
317 store = _make_store()
318 source = b"foo"
319 name = _text_node(b"foo")
320 fn_node = MockNode("function_item", start_point=(0, 0), end_point=(0, 10))
321 fn_node.set_field("name", name)
322 root = MockNode("source_file", children=[fn_node])
323 stats = {"functions": 0, "classes": 0, "edges": 0}
324 parser._walk(root, source, "lib.rs", store, stats, impl_type=None)
325 assert stats["functions"] == 1
326
327 def test_walk_handles_impl_item(self):
328 parser = _make_parser()
329 store = _make_store()
330 source = b"MyStruct"
331 type_node = MockNode("type_identifier", start_byte=0, end_byte=8)
332 body = MockNode("declaration_list", children=[])
333 impl_node = MockNode("impl_item")
334 impl_node.set_field("type", type_node)
335 impl_node.set_field("body", body)
336 root = MockNode("source_file", children=[impl_node])
337 stats = {"functions": 0, "classes": 0, "edges": 0}
338 parser._walk(root, source, "lib.rs", store, stats, impl_type=None)
339 # No functions in body, just verifies dispatch doesn't crash
340 assert stats["functions"] == 0
341
342 def test_walk_handles_struct_item(self):
343 parser = _make_parser()
344 store = _make_store()
345 source = b"Foo"
346 name = _text_node(b"Foo", "type_identifier")
347 node = MockNode("struct_item", start_point=(0, 0), end_point=(0, 10))
348 node.set_field("name", name)
349 _parent = MockNode("source_file", children=[node])
350 root = MockNode("source_file", children=[node])
351 stats = {"functions": 0, "classes": 0, "edges": 0}
352 parser._walk(root, source, "lib.rs", store, stats, impl_type=None)
353 assert stats["classes"] == 1
354
355 def test_walk_handles_use_declaration(self):
356 parser = _make_parser()
357 store = _make_store()
358 source = b"use std::io;"
359 use_node = MockNode("use_declaration", start_byte=0, end_byte=12)
360 root = MockNode("source_file", children=[use_node])
361 stats = {"functions": 0, "classes": 0, "edges": 0}
362 parser._walk(root, source, "lib.rs", store, stats, impl_type=None)
363 assert stats["edges"] == 1
364
365 def test_walk_recurses_into_unknown_nodes(self):
366 parser = _make_parser()
367 store = _make_store()
368 source = b"foo"
369 name = _text_node(b"foo")
370 fn_node = MockNode("function_item", start_point=(0, 0), end_point=(0, 10))
371 fn_node.set_field("name", name)
372 wrapper = MockNode("mod_item", children=[fn_node])
373 root = MockNode("source_file", children=[wrapper])
374 stats = {"functions": 0, "classes": 0, "edges": 0}
375 parser._walk(root, source, "lib.rs", store, stats, impl_type=None)
376 assert stats["functions"] == 1
377
--- tests/test_typescript_parser.py
+++ tests/test_typescript_parser.py
@@ -311,5 +311,243 @@
311311
children=[MockNode("statement_block")])
312312
stats = {"functions": 0, "classes": 0, "edges": 0}
313313
parser._extract_calls(fn_node, b"", "app.ts", "foo",
314314
NodeLabel.Function, store, stats)
315315
assert stats["edges"] == 0
316
+
317
+
318
+# ── _get_ts_language happy paths ──────────────────────────────────────────────
319
+
320
+class TestTsGetLanguageHappyPath:
321
+ def test_returns_typescript_language(self):
322
+ from navegador.ingestion.typescript import _get_ts_language
323
+ mock_tsts = MagicMock()
324
+ mock_ts = MagicMock()
325
+ with patch.dict("sys.modules", {
326
+ "tree_sitter_typescript": mock_tsts,
327
+ "tree_sitter": mock_ts,
328
+ }):
329
+ result = _get_ts_language("typescript")
330
+ assert result is mock_ts.Language.return_value
331
+
332
+ def test_returns_javascript_language(self):
333
+ from navegador.ingestion.typescript import _get_ts_language
334
+ mock_tsjs = MagicMock()
335
+ mock_ts = MagicMock()
336
+ with patch.dict("sys.modules", {
337
+ "tree_sitter_javascript": mock_tsjs,
338
+ "tree_sitter": mock_ts,
339
+ }):
340
+ result = _get_ts_language("javascript")
341
+ assert result is mock_ts.Language.return_value
342
+
343
+
344
+# ── TypeScriptParser init and parse_file ─────────────────────────────────────
345
+
346
+class TestTsParserInit:
347
+ def test_init_creates_parser(self):
348
+ mock_tsts = MagicMock()
349
+ mock_ts = MagicMock()
350
+ with patch.dict("sys.modules", {
351
+ "tree_sitter_typescript": mock_tsts,
352
+ "tree_sitter": mock_ts,
353
+ }):
354
+ from navegador.ingestion.typescript import TypeScriptParser
355
+ parser = TypeScriptParser("typescript")
356
+ assert parser._parser is mock_ts.Parser.return_value
357
+ assert parser._language == "typescript"
358
+
359
+ def test_parse_file_creates_file_node(self):
360
+ import tempfile
361
+ from pathlib import Path
362
+
363
+ from navegador.graph.schema import NodeLabel
364
+ parser = _make_parser("typescript")
365
+ store = _make_store()
366
+ mock_tree = MagicMock()
367
+ mock_tree.root_node.type = "program"
368
+ mock_tree.root_node.children = []
369
+ parser._parser.parse.return_value = mock_tree
370
+ with tempfile.NamedTemporaryFile(suffix=".ts", delete=False) as f:
371
+ f.write(b"const x = 1;\n")
372
+ fpath = Path(f.name)
373
+ try:
374
+ parser.parse_file(fpath, fpath.parent, store)
375
+ store.create_node.assert_called_once()
376
+ assert store.create_node.call_args[0][0] == NodeLabel.File
377
+ assert store.create_node.call_args[0][1]["language"] == "typescript"
378
+ finally:
379
+ fpath.unlink()
380
+
381
+
382
+# ── _walk dispatch ────────────────────────────────────────────────────────────
383
+
384
+class TestTsWalkDispatch:
385
+ def test_walk_handles_class_declaration(self):
386
+ parser = _make_parser()
387
+ store = _make_store()
388
+ source = b"MyClass"
389
+ name = MockNode("type_identifier", start_byte=0, end_byte=7)
390
+ body = MockNode("class_body", children=[])
391
+ node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 20))
392
+ node.children = [name, body]
393
+ root = MockNode("program", children=[node])
394
+ stats = {"functions": 0, "classes": 0, "edges": 0}
395
+ parser._walk(root, source, "app.ts", store, stats, class_name=None)
396
+ assert stats["classes"] == 1
397
+
398
+ def test_walk_handles_interface_declaration(self):
399
+ parser = _make_parser()
400
+ store = _make_store()
401
+ source = b"MyInterface"
402
+ name = MockNode("type_identifier", start_byte=0, end_byte=11)
403
+ node = MockNode("interface_declaration", start_point=(0, 0), end_point=(0, 25))
404
+ node.children = [name]
405
+ root = MockNode("program", children=[node])
406
+ stats = {"functions": 0, "classes": 0, "edges": 0}
407
+ parser._walk(root, source, "app.ts", store, stats, class_name=None)
408
+ assert stats["classes"] == 1
409
+
410
+ def test_walk_handles_function_declaration(self):
411
+ parser = _make_parser()
412
+ store = _make_store()
413
+ source = b"myFn"
414
+ name = MockNode("identifier", start_byte=0, end_byte=4)
415
+ node = MockNode("function_declaration", start_point=(0, 0), end_point=(0, 20))
416
+ node.children = [name]
417
+ root = MockNode("program", children=[node])
418
+ stats = {"functions": 0, "classes": 0, "edges": 0}
419
+ parser._walk(root, source, "app.ts", store, stats, class_name=None)
420
+ assert stats["functions"] == 1
421
+
422
+ def test_walk_handles_lexical_declaration(self):
423
+ parser = _make_parser()
424
+ store = _make_store()
425
+ source = b"arrowFn"
426
+ name_node = MockNode("identifier", start_byte=0, end_byte=7)
427
+ arrow = MockNode("arrow_function")
428
+ declarator = MockNode("variable_declarator")
429
+ declarator.set_field("name", name_node)
430
+ declarator.set_field("value", arrow)
431
+ node = MockNode("lexical_declaration", start_point=(0, 0), end_point=(0, 30))
432
+ node.children = [declarator]
433
+ root = MockNode("program", children=[node])
434
+ stats = {"functions": 0, "classes": 0, "edges": 0}
435
+ parser._walk(root, source, "app.ts", store, stats, class_name=None)
436
+ assert stats["functions"] == 1
437
+
438
+ def test_walk_handles_import_statement(self):
439
+ parser = _make_parser()
440
+ store = _make_store()
441
+ source = b'"./utils"'
442
+ str_node = MockNode("string", start_byte=0, end_byte=9)
443
+ node = MockNode("import_statement", start_point=(0, 0), end_point=(0, 25))
444
+ node.children = [str_node]
445
+ root = MockNode("program", children=[node])
446
+ stats = {"functions": 0, "classes": 0, "edges": 0}
447
+ parser._walk(root, source, "app.ts", store, stats, class_name=None)
448
+ assert stats["edges"] == 1
449
+
450
+ def test_walk_handles_export_statement(self):
451
+ parser = _make_parser()
452
+ store = _make_store()
453
+ source = b"myFn"
454
+ name = MockNode("identifier", start_byte=0, end_byte=4)
455
+ fn_decl = MockNode("function_declaration", start_point=(0, 0), end_point=(0, 20))
456
+ fn_decl.children = [name]
457
+ export_node = MockNode("export_statement")
458
+ export_node.children = [MockNode("export"), fn_decl]
459
+ root = MockNode("program", children=[export_node])
460
+ stats = {"functions": 0, "classes": 0, "edges": 0}
461
+ parser._walk(root, source, "app.ts", store, stats, class_name=None)
462
+ assert stats["functions"] == 1
463
+
464
+ def test_walk_recurses_into_children(self):
465
+ parser = _make_parser()
466
+ store = _make_store()
467
+ source = b"myFn"
468
+ name = MockNode("identifier", start_byte=0, end_byte=4)
469
+ fn = MockNode("function_declaration", start_point=(0, 0), end_point=(0, 20))
470
+ fn.children = [name]
471
+ wrapper = MockNode("unknown_node", children=[fn])
472
+ root = MockNode("program", children=[wrapper])
473
+ stats = {"functions": 0, "classes": 0, "edges": 0}
474
+ parser._walk(root, source, "app.ts", store, stats, class_name=None)
475
+ assert stats["functions"] == 1
476
+
477
+
478
+# ── _handle_function keyword name with no follow-up identifier ────────────────
479
+
480
+class TestTsHandleFunctionKeywordThenNone:
481
+ def test_skips_when_keyword_name_has_no_second_identifier(self):
482
+ parser = _make_parser()
483
+ store = _make_store()
484
+ source = b"constructor"
485
+ # Node whose only identifier child is the keyword "constructor"
486
+ keyword_name = MockNode("property_identifier", start_byte=0, end_byte=11)
487
+ node = MockNode("method_definition", start_point=(0, 0), end_point=(0, 20))
488
+ node.children = [keyword_name]
489
+ stats = {"functions": 0, "classes": 0, "edges": 0}
490
+ parser._handle_function(node, source, "app.ts", store, stats, class_name=None)
491
+ # "constructor" is a keyword — looks for next identifier, finds none → skips
492
+ assert stats["functions"] == 0
493
+
494
+
495
+class TestTsHandleFunctionKeywordWithSuccessor:
496
+ def test_uses_second_identifier_when_first_is_keyword(self):
497
+ parser = _make_parser()
498
+ store = _make_store()
499
+ source = b"get foo"
500
+ keyword_name = MockNode("identifier", start_byte=0, end_byte=3)
501
+ real_name = MockNode("identifier", start_byte=4, end_byte=7)
502
+ node = MockNode("method_definition", start_point=(0, 0), end_point=(0, 10))
503
+ node.children = [keyword_name, real_name]
504
+ stats = {"functions": 0, "classes": 0, "edges": 0}
505
+ parser._handle_function(node, source, "app.ts", store, stats, class_name=None)
506
+ # "get" is a keyword → picks next identifier "foo"
507
+ assert stats["functions"] == 1
508
+ props = store.create_node.call_args[0][1]
509
+ assert props["name"] == "foo"
510
+
511
+
512
+class TestTsWalkMethodDefinition:
513
+ def test_walk_dispatches_method_definition(self):
514
+ parser = _make_parser()
515
+ store = _make_store()
516
+ source = b"get foo"
517
+ keyword_name = MockNode("identifier", start_byte=0, end_byte=3)
518
+ real_name = MockNode("identifier", start_byte=4, end_byte=7)
519
+ node = MockNode("method_definition", start_point=(0, 0), end_point=(0, 10))
520
+ node.children = [keyword_name, real_name]
521
+ root = MockNode("program", children=[node])
522
+ stats = {"functions": 0, "classes": 0, "edges": 0}
523
+ parser._walk(root, source, "app.ts", store, stats, class_name=None)
524
+ assert stats["functions"] == 1
525
+
526
+
527
+class TestTsHandleLexicalContinueBranches:
528
+ def test_skips_non_variable_declarator_children(self):
529
+ parser = _make_parser()
530
+ store = _make_store()
531
+ source = b"const"
532
+ # Child is not a variable_declarator
533
+ other_child = MockNode("identifier", start_byte=0, end_byte=5)
534
+ node = MockNode("lexical_declaration", start_point=(0, 0), end_point=(0, 10))
535
+ node.children = [other_child]
536
+ stats = {"functions": 0, "classes": 0, "edges": 0}
537
+ parser._handle_lexical(node, source, "app.ts", store, stats)
538
+ assert stats["functions"] == 0
539
+
540
+ def test_skips_declarator_without_value_node(self):
541
+ parser = _make_parser()
542
+ store = _make_store()
543
+ source = b"x"
544
+ name_node = MockNode("identifier", start_byte=0, end_byte=1)
545
+ # declarator with a name but no value field
546
+ declarator = MockNode("variable_declarator")
547
+ declarator._fields["name"] = name_node
548
+ # no "value" field set
549
+ node = MockNode("lexical_declaration", start_point=(0, 0), end_point=(0, 5))
550
+ node.children = [declarator]
551
+ stats = {"functions": 0, "classes": 0, "edges": 0}
552
+ parser._handle_lexical(node, source, "app.ts", store, stats)
553
+ assert stats["functions"] == 0
316554
--- tests/test_typescript_parser.py
+++ tests/test_typescript_parser.py
@@ -311,5 +311,243 @@
311 children=[MockNode("statement_block")])
312 stats = {"functions": 0, "classes": 0, "edges": 0}
313 parser._extract_calls(fn_node, b"", "app.ts", "foo",
314 NodeLabel.Function, store, stats)
315 assert stats["edges"] == 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
--- tests/test_typescript_parser.py
+++ tests/test_typescript_parser.py
@@ -311,5 +311,243 @@
311 children=[MockNode("statement_block")])
312 stats = {"functions": 0, "classes": 0, "edges": 0}
313 parser._extract_calls(fn_node, b"", "app.ts", "foo",
314 NodeLabel.Function, store, stats)
315 assert stats["edges"] == 0
316
317
318 # ── _get_ts_language happy paths ──────────────────────────────────────────────
319
320 class TestTsGetLanguageHappyPath:
321 def test_returns_typescript_language(self):
322 from navegador.ingestion.typescript import _get_ts_language
323 mock_tsts = MagicMock()
324 mock_ts = MagicMock()
325 with patch.dict("sys.modules", {
326 "tree_sitter_typescript": mock_tsts,
327 "tree_sitter": mock_ts,
328 }):
329 result = _get_ts_language("typescript")
330 assert result is mock_ts.Language.return_value
331
332 def test_returns_javascript_language(self):
333 from navegador.ingestion.typescript import _get_ts_language
334 mock_tsjs = MagicMock()
335 mock_ts = MagicMock()
336 with patch.dict("sys.modules", {
337 "tree_sitter_javascript": mock_tsjs,
338 "tree_sitter": mock_ts,
339 }):
340 result = _get_ts_language("javascript")
341 assert result is mock_ts.Language.return_value
342
343
344 # ── TypeScriptParser init and parse_file ─────────────────────────────────────
345
346 class TestTsParserInit:
347 def test_init_creates_parser(self):
348 mock_tsts = MagicMock()
349 mock_ts = MagicMock()
350 with patch.dict("sys.modules", {
351 "tree_sitter_typescript": mock_tsts,
352 "tree_sitter": mock_ts,
353 }):
354 from navegador.ingestion.typescript import TypeScriptParser
355 parser = TypeScriptParser("typescript")
356 assert parser._parser is mock_ts.Parser.return_value
357 assert parser._language == "typescript"
358
359 def test_parse_file_creates_file_node(self):
360 import tempfile
361 from pathlib import Path
362
363 from navegador.graph.schema import NodeLabel
364 parser = _make_parser("typescript")
365 store = _make_store()
366 mock_tree = MagicMock()
367 mock_tree.root_node.type = "program"
368 mock_tree.root_node.children = []
369 parser._parser.parse.return_value = mock_tree
370 with tempfile.NamedTemporaryFile(suffix=".ts", delete=False) as f:
371 f.write(b"const x = 1;\n")
372 fpath = Path(f.name)
373 try:
374 parser.parse_file(fpath, fpath.parent, store)
375 store.create_node.assert_called_once()
376 assert store.create_node.call_args[0][0] == NodeLabel.File
377 assert store.create_node.call_args[0][1]["language"] == "typescript"
378 finally:
379 fpath.unlink()
380
381
382 # ── _walk dispatch ────────────────────────────────────────────────────────────
383
384 class TestTsWalkDispatch:
385 def test_walk_handles_class_declaration(self):
386 parser = _make_parser()
387 store = _make_store()
388 source = b"MyClass"
389 name = MockNode("type_identifier", start_byte=0, end_byte=7)
390 body = MockNode("class_body", children=[])
391 node = MockNode("class_declaration", start_point=(0, 0), end_point=(0, 20))
392 node.children = [name, body]
393 root = MockNode("program", children=[node])
394 stats = {"functions": 0, "classes": 0, "edges": 0}
395 parser._walk(root, source, "app.ts", store, stats, class_name=None)
396 assert stats["classes"] == 1
397
398 def test_walk_handles_interface_declaration(self):
399 parser = _make_parser()
400 store = _make_store()
401 source = b"MyInterface"
402 name = MockNode("type_identifier", start_byte=0, end_byte=11)
403 node = MockNode("interface_declaration", start_point=(0, 0), end_point=(0, 25))
404 node.children = [name]
405 root = MockNode("program", children=[node])
406 stats = {"functions": 0, "classes": 0, "edges": 0}
407 parser._walk(root, source, "app.ts", store, stats, class_name=None)
408 assert stats["classes"] == 1
409
410 def test_walk_handles_function_declaration(self):
411 parser = _make_parser()
412 store = _make_store()
413 source = b"myFn"
414 name = MockNode("identifier", start_byte=0, end_byte=4)
415 node = MockNode("function_declaration", start_point=(0, 0), end_point=(0, 20))
416 node.children = [name]
417 root = MockNode("program", children=[node])
418 stats = {"functions": 0, "classes": 0, "edges": 0}
419 parser._walk(root, source, "app.ts", store, stats, class_name=None)
420 assert stats["functions"] == 1
421
422 def test_walk_handles_lexical_declaration(self):
423 parser = _make_parser()
424 store = _make_store()
425 source = b"arrowFn"
426 name_node = MockNode("identifier", start_byte=0, end_byte=7)
427 arrow = MockNode("arrow_function")
428 declarator = MockNode("variable_declarator")
429 declarator.set_field("name", name_node)
430 declarator.set_field("value", arrow)
431 node = MockNode("lexical_declaration", start_point=(0, 0), end_point=(0, 30))
432 node.children = [declarator]
433 root = MockNode("program", children=[node])
434 stats = {"functions": 0, "classes": 0, "edges": 0}
435 parser._walk(root, source, "app.ts", store, stats, class_name=None)
436 assert stats["functions"] == 1
437
438 def test_walk_handles_import_statement(self):
439 parser = _make_parser()
440 store = _make_store()
441 source = b'"./utils"'
442 str_node = MockNode("string", start_byte=0, end_byte=9)
443 node = MockNode("import_statement", start_point=(0, 0), end_point=(0, 25))
444 node.children = [str_node]
445 root = MockNode("program", children=[node])
446 stats = {"functions": 0, "classes": 0, "edges": 0}
447 parser._walk(root, source, "app.ts", store, stats, class_name=None)
448 assert stats["edges"] == 1
449
450 def test_walk_handles_export_statement(self):
451 parser = _make_parser()
452 store = _make_store()
453 source = b"myFn"
454 name = MockNode("identifier", start_byte=0, end_byte=4)
455 fn_decl = MockNode("function_declaration", start_point=(0, 0), end_point=(0, 20))
456 fn_decl.children = [name]
457 export_node = MockNode("export_statement")
458 export_node.children = [MockNode("export"), fn_decl]
459 root = MockNode("program", children=[export_node])
460 stats = {"functions": 0, "classes": 0, "edges": 0}
461 parser._walk(root, source, "app.ts", store, stats, class_name=None)
462 assert stats["functions"] == 1
463
464 def test_walk_recurses_into_children(self):
465 parser = _make_parser()
466 store = _make_store()
467 source = b"myFn"
468 name = MockNode("identifier", start_byte=0, end_byte=4)
469 fn = MockNode("function_declaration", start_point=(0, 0), end_point=(0, 20))
470 fn.children = [name]
471 wrapper = MockNode("unknown_node", children=[fn])
472 root = MockNode("program", children=[wrapper])
473 stats = {"functions": 0, "classes": 0, "edges": 0}
474 parser._walk(root, source, "app.ts", store, stats, class_name=None)
475 assert stats["functions"] == 1
476
477
478 # ── _handle_function keyword name with no follow-up identifier ────────────────
479
480 class TestTsHandleFunctionKeywordThenNone:
481 def test_skips_when_keyword_name_has_no_second_identifier(self):
482 parser = _make_parser()
483 store = _make_store()
484 source = b"constructor"
485 # Node whose only identifier child is the keyword "constructor"
486 keyword_name = MockNode("property_identifier", start_byte=0, end_byte=11)
487 node = MockNode("method_definition", start_point=(0, 0), end_point=(0, 20))
488 node.children = [keyword_name]
489 stats = {"functions": 0, "classes": 0, "edges": 0}
490 parser._handle_function(node, source, "app.ts", store, stats, class_name=None)
491 # "constructor" is a keyword — looks for next identifier, finds none → skips
492 assert stats["functions"] == 0
493
494
495 class TestTsHandleFunctionKeywordWithSuccessor:
496 def test_uses_second_identifier_when_first_is_keyword(self):
497 parser = _make_parser()
498 store = _make_store()
499 source = b"get foo"
500 keyword_name = MockNode("identifier", start_byte=0, end_byte=3)
501 real_name = MockNode("identifier", start_byte=4, end_byte=7)
502 node = MockNode("method_definition", start_point=(0, 0), end_point=(0, 10))
503 node.children = [keyword_name, real_name]
504 stats = {"functions": 0, "classes": 0, "edges": 0}
505 parser._handle_function(node, source, "app.ts", store, stats, class_name=None)
506 # "get" is a keyword → picks next identifier "foo"
507 assert stats["functions"] == 1
508 props = store.create_node.call_args[0][1]
509 assert props["name"] == "foo"
510
511
512 class TestTsWalkMethodDefinition:
513 def test_walk_dispatches_method_definition(self):
514 parser = _make_parser()
515 store = _make_store()
516 source = b"get foo"
517 keyword_name = MockNode("identifier", start_byte=0, end_byte=3)
518 real_name = MockNode("identifier", start_byte=4, end_byte=7)
519 node = MockNode("method_definition", start_point=(0, 0), end_point=(0, 10))
520 node.children = [keyword_name, real_name]
521 root = MockNode("program", children=[node])
522 stats = {"functions": 0, "classes": 0, "edges": 0}
523 parser._walk(root, source, "app.ts", store, stats, class_name=None)
524 assert stats["functions"] == 1
525
526
527 class TestTsHandleLexicalContinueBranches:
528 def test_skips_non_variable_declarator_children(self):
529 parser = _make_parser()
530 store = _make_store()
531 source = b"const"
532 # Child is not a variable_declarator
533 other_child = MockNode("identifier", start_byte=0, end_byte=5)
534 node = MockNode("lexical_declaration", start_point=(0, 0), end_point=(0, 10))
535 node.children = [other_child]
536 stats = {"functions": 0, "classes": 0, "edges": 0}
537 parser._handle_lexical(node, source, "app.ts", store, stats)
538 assert stats["functions"] == 0
539
540 def test_skips_declarator_without_value_node(self):
541 parser = _make_parser()
542 store = _make_store()
543 source = b"x"
544 name_node = MockNode("identifier", start_byte=0, end_byte=1)
545 # declarator with a name but no value field
546 declarator = MockNode("variable_declarator")
547 declarator._fields["name"] = name_node
548 # no "value" field set
549 node = MockNode("lexical_declaration", start_point=(0, 0), end_point=(0, 5))
550 node.children = [declarator]
551 stats = {"functions": 0, "classes": 0, "edges": 0}
552 parser._handle_lexical(node, source, "app.ts", store, stats)
553 assert stats["functions"] == 0
554

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button