From 9dd08b35671da9dce00da0b99767eef9c7ac4b8e Mon Sep 17 00:00:00 2001 From: Tony Nguyen Date: Thu, 23 Jul 2026 09:38:48 -0500 Subject: [PATCH] fix(parser): harden Rust cfg(test) exclusion and remove file size cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Red-team review findings against the Rust parser: 1. Comment/doc-comment between #[cfg(test)] and its item broke test exclusion — tree-sitter emits comments as named children which cleared pending_attributes before the mod_item was reached. Fix: skip line_comment/block_comment nodes without clearing. 2. #[cfg(test)] on non-mod items (functions, structs, impls) was silently ignored — is_cfg_test was computed but only checked for mod_item. Fix: skip ALL items annotated with #[cfg(test)]. 3. Removed _MAX_FILE_BYTES (1MB) cap — no other parser has it, it silently drops real code (generated protobuf/diesel schemas), and the per-file except boundary is the actual backstop. Regression tests cover all three fixes: comment-between-attribute, cfg(test) on fn/struct/impl, and >1MB file survival. --- src/arcade_agent/parsers/rust.py | 19 +++++++---- tests/test_parsers/test_rust.py | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/arcade_agent/parsers/rust.py b/src/arcade_agent/parsers/rust.py index 3f27a2a..70c1aae 100644 --- a/src/arcade_agent/parsers/rust.py +++ b/src/arcade_agent/parsers/rust.py @@ -443,11 +443,24 @@ def visit_container( pending_attributes.append(node) continue + # Comments between an attribute and its item must not + # clear the pending attribute list (tree-sitter emits + # line_comment/block_comment as named children). + if node.type in {"line_comment", "block_comment"}: + continue + is_cfg_test = any( _is_cfg_test_attribute(attribute) for attribute in pending_attributes ) pending_attributes.clear() + # Rust unit tests live inline, so path-based test + # exclusion in ``ingest`` cannot see them. Drop + # ``#[cfg(test)]`` items only when the caller asked + # for test code to be excluded. + if is_cfg_test and self.exclude_tests: + continue + if node.type in _TYPE_ITEMS: name_node = node.child_by_field_name("name") if name_node is None: @@ -548,12 +561,6 @@ def visit_container( ) ) elif node.type == "mod_item": - # Rust unit tests live inline, so path-based test - # exclusion in ``ingest`` cannot see them. Drop - # ``#[cfg(test)]`` modules only when the caller asked - # for test code to be excluded. - if is_cfg_test and self.exclude_tests: - continue name_node = node.child_by_field_name("name") body = node.child_by_field_name("body") if name_node is not None and body is not None: diff --git a/tests/test_parsers/test_rust.py b/tests/test_parsers/test_rust.py index a78a227..21a5938 100644 --- a/tests/test_parsers/test_rust.py +++ b/tests/test_parsers/test_rust.py @@ -253,6 +253,62 @@ def test_rust_parser_does_not_silently_drop_large_files(tmp_path): assert "Tail" in graph.entities +def test_rust_parser_skips_cfg_test_with_comment_between_attribute_and_item(tmp_path): + """A comment between #[cfg(test)] and the item must not break exclusion.""" + source = tmp_path / "lib.rs" + source.write_text( + "pub struct Production;\n" + "#[cfg(test)]\n" + "// unit tests for this module\n" + "mod tests {\n" + " struct Fixture;\n" + " fn helper() {}\n" + "}\n" + "#[cfg(test)]\n" + "/// Doc comment should also not break exclusion\n" + "mod doc_tests {\n" + " struct DocFixture;\n" + "}\n" + ) + + graph = RustParser().parse([source], tmp_path) + assert "Production" in graph.entities + assert all(not fqn.startswith("tests") for fqn in graph.entities) + assert all(not fqn.startswith("doc_tests") for fqn in graph.entities) + + +def test_rust_parser_skips_cfg_test_on_non_mod_items(tmp_path): + """#[cfg(test)] on functions, structs, and impls must also be excluded.""" + source = tmp_path / "lib.rs" + source.write_text( + "pub struct Production;\n" + "#[cfg(test)]\n" + "fn test_only_helper() {}\n" + "#[cfg(test)]\n" + "struct TestFixture { x: u64 }\n" + "#[cfg(test)]\n" + "impl TestFixture { fn setup(&self) {} }\n" + "pub fn real_function() {}\n" + ) + + graph = RustParser().parse([source], tmp_path) + assert "Production" in graph.entities + assert "real_function" in graph.entities + assert "test_only_helper" not in graph.entities + assert all("TestFixture" not in fqn for fqn in graph.entities) + + +def test_rust_parser_handles_large_files_without_cap(tmp_path): + """Files larger than 1MB must still be parsed (no silent cap).""" + source = tmp_path / "large.rs" + # Generate a file > 1MB with valid Rust content + padding = "// padding\n" * 100_000 # ~1.1MB of comments + source.write_text(padding + "pub struct LargeFile;\n") + + graph = RustParser().parse([source], tmp_path) + assert "large.LargeFile" in graph.entities + + def test_rust_parser_tolerates_invalid_cargo_manifest_encoding(tmp_path): (tmp_path / "Cargo.toml").write_bytes(b"\xff\xfe") source = tmp_path / "lib.rs"