Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions code_review_graph/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,10 @@ class EdgeInfo:
_TEST_ANNOTATIONS = frozenset({
"Test", "ParameterizedTest", "RepeatedTest", "TestFactory",
"org.junit.Test", "org.junit.jupiter.api.Test",
# Rust: built-in `#[test]` plus common async-runtime + framework
# variants. Stripped of the `#[ ]` wrapper before lookup.
"test", "tokio::test", "async_std::test",
"rstest", "rstest::rstest", "proptest",
})

# Spring stereotype annotations that mark classes as managed beans
Expand Down Expand Up @@ -4341,6 +4345,23 @@ def _extract_functions(
if sib.type == "decorator":
text = sib.text.decode("utf-8", errors="replace")
deco_list.append(text.lstrip("@").strip())
# Rust: attributes are preceding siblings of function_item, not
# children. Walk back through `attribute_item` nodes and strip the
# `#[ ]` (or `#![ ]`) wrapper.
if language == "rust":
sib = child.prev_sibling
while sib is not None and sib.type == "attribute_item":
text = sib.text.decode("utf-8", errors="replace").strip()
if text.startswith("#!["):
inner = text[3:].rstrip()
elif text.startswith("#["):
inner = text[2:].rstrip()
else:
inner = text
if inner.endswith("]"):
inner = inner[:-1]
deco_list.append(inner.strip())
sib = sib.prev_sibling
if deco_list:
decorators = tuple(deco_list)

Expand Down
23 changes: 23 additions & 0 deletions tests/fixtures/sample_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,26 @@ pub fn create_user(repo: &mut impl Repository, name: &str, email: &str) -> User
repo.save(user.clone());
user
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn new_repo_is_empty() {
let repo = InMemoryRepo::new();
assert!(repo.find_by_id(1).is_none());
}

#[test]
fn create_user_saves_to_repo() {
let mut repo = InMemoryRepo::new();
let user = create_user(&mut repo, "alice", "a@b.c");
assert_eq!(user.name, "alice");
}

#[tokio::test]
async fn async_test_is_detected() {
assert!(true);
}
}
20 changes: 20 additions & 0 deletions tests/test_multilang.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,26 @@ def test_finds_calls(self):
calls = [e for e in self.edges if e.kind == "CALLS"]
assert len(calls) >= 3

def test_detects_test_attribute(self):
tests = [n for n in self.nodes if n.kind == "Test"]
names = {t.name for t in tests}
assert "new_repo_is_empty" in names
assert "create_user_saves_to_repo" in names
assert all(t.is_test for t in tests)

def test_detects_tokio_test_attribute(self):
tests = {n.name for n in self.nodes if n.kind == "Test"}
assert "async_test_is_detected" in tests

def test_non_test_functions_not_misclassified(self):
funcs = {n.name for n in self.nodes if n.kind == "Function"}
assert "create_user" in funcs
assert "new" in funcs
# `create_user` carries no `#[test]` — must stay Function.
for n in self.nodes:
if n.name == "create_user":
assert not n.is_test


class TestJavaParsing:
def setup_method(self):
Expand Down