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.
Commit
7ae008029f20f95cac8db3109567e469c2cf7b1374684e95c7af6e67e33e6ea7
Parent
8fe142010912296…
8 files changed
+182
+182
+36
+24
+155
+149
+119
+238
+182
| --- tests/test_context.py | ||
| +++ tests/test_context.py | ||
| @@ -280,5 +280,187 @@ | ||
| 280 | 280 | store = _mock_store(rows) |
| 281 | 281 | loader = ContextLoader(store) |
| 282 | 282 | bundle = loader.load_domain("auth") |
| 283 | 283 | assert bundle.target.name == "auth" |
| 284 | 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 | |
| 285 | 467 |
| --- 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 |
+182
| --- tests/test_go_parser.py | ||
| +++ tests/test_go_parser.py | ||
| @@ -231,5 +231,187 @@ | ||
| 231 | 231 | start_point=(0, 0), end_point=(0, 17)) |
| 232 | 232 | root = MockNode("source_file", children=[decl]) |
| 233 | 233 | stats = {"functions": 0, "classes": 0, "edges": 0} |
| 234 | 234 | parser._walk(root, source, "main.go", store, stats) |
| 235 | 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 | |
| 236 | 418 |
| --- 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 @@ | ||
| 320 | 320 | "navegador.ingestion.java": MagicMock(JavaParser=mock_java_class) |
| 321 | 321 | }): |
| 322 | 322 | result = ingester._get_parser("java") |
| 323 | 323 | assert result is mock_java_parser |
| 324 | 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()) | |
| 325 | 361 |
| --- 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 @@ | ||
| 172 | 172 | def test_annotate_invalid_label_raises(self): |
| 173 | 173 | store = _mock_store() |
| 174 | 174 | k = KnowledgeIngester(store) |
| 175 | 175 | with pytest.raises(ValueError): |
| 176 | 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 | |
| 177 | 201 |
| --- 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 |
+155
| --- tests/test_java_parser.py | ||
| +++ tests/test_java_parser.py | ||
| @@ -251,5 +251,160 @@ | ||
| 251 | 251 | node = MockNode("method_declaration") |
| 252 | 252 | node.set_field("body", MockNode("block")) |
| 253 | 253 | stats = {"functions": 0, "classes": 0, "edges": 0} |
| 254 | 254 | parser._extract_calls(node, b"", "X.java", "foo", store, stats) |
| 255 | 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 | |
| 256 | 411 |
| --- 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 |
+149
| --- tests/test_python_parser.py | ||
| +++ tests/test_python_parser.py | ||
| @@ -265,5 +265,154 @@ | ||
| 265 | 265 | start_point=(0, 0), end_point=(0, 0)) |
| 266 | 266 | root = MockNode("module", children=[fn_node]) |
| 267 | 267 | stats = {"functions": 0, "classes": 0, "edges": 0} |
| 268 | 268 | parser._walk(root, b"def my_fn(): pass", "app.py", store, stats, class_name=None) |
| 269 | 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 | |
| 270 | 419 |
| --- 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 |
+119
| --- tests/test_rust_parser.py | ||
| +++ tests/test_rust_parser.py | ||
| @@ -253,5 +253,124 @@ | ||
| 253 | 253 | parser._extract_calls(fn_node, source, "lib.rs", "foo", |
| 254 | 254 | NodeLabel.Function, store, stats) |
| 255 | 255 | # "Repo::save" → callee = "save" |
| 256 | 256 | edge_call = store.create_edge.call_args[0] |
| 257 | 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 | |
| 258 | 377 |
| --- 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 @@ | ||
| 311 | 311 | children=[MockNode("statement_block")]) |
| 312 | 312 | stats = {"functions": 0, "classes": 0, "edges": 0} |
| 313 | 313 | parser._extract_calls(fn_node, b"", "app.ts", "foo", |
| 314 | 314 | NodeLabel.Function, store, stats) |
| 315 | 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 | |
| 316 | 554 |
| --- 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 |