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
69 changes: 61 additions & 8 deletions crates/derive-impl/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1986,14 +1986,21 @@ where
while let Some((_, attr)) = iter.peek() {
// take all cfgs but no py items
let attr = *attr;
let attr_name = if let Some(ident) = attr.get_ident() {
ident.to_string()
} else {
continue;
};
if attr_name == "cfg" {
cfgs.push(attr.clone());
} else if ALL_ALLOWED_NAMES.contains(&attr_name.as_str()) {
if let Some(ident) = attr.get_ident() {
let attr_name = ident.to_string();
if attr_name == "cfg" {
cfgs.push(attr.clone());
} else if ALL_ALLOWED_NAMES.contains(&attr_name.as_str()) {
break;
}
} else if attr
.path()
.segments
.last()
.is_some_and(|s| ALL_ALLOWED_NAMES.contains(&s.ident.to_string().as_str()))
{
// Multi-segment path whose last segment is a py* name (e.g. `vm::pymethod`).
// Stop here so the for-loop below can emit a diagnostic with the correct span.
break;
}
iter.next();
Expand All @@ -2004,6 +2011,28 @@ where
let attr_name = if let Some(ident) = attr.get_ident() {
ident.to_string()
} else {
// Multi-segment path: if the last segment is a known py* name, the user
// likely wrote `#[vm::pymethod]` instead of `#[pymethod]`. Proc-macros
// cannot resolve namespace aliases, so the qualified form is never supported.
if let Some(last_seg) = attr.path().segments.last() {
let last_name = last_seg.ident.to_string();
if ALL_ALLOWED_NAMES.contains(&last_name.as_str()) {
let full_path = attr
.path()
.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.join("::");
bail_span!(
attr,
"found `#[{}]`, use `#[{}]` instead; \
proc-macros cannot resolve namespace aliases",
full_path,
last_name
);
}
}
continue;
};
if attr_name == "cfg" {
Expand Down Expand Up @@ -2044,3 +2073,27 @@ fn parse_vec_ident(
})?
.to_string())
}

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

// Regression test: path-qualified inner attrs like `#[vm::pymethod]` must return an error.
// Before the fix, `attrs_to_content_items` looped forever on such attributes.
#[test]
fn path_qualified_py_attr_returns_error() {
let attr: syn::Attribute = syn::parse_quote!(#[vm::pymethod]);
let result =
attrs_to_content_items(&[attr], |i, name| (i, name));
Comment on lines +2086 to +2087

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[reviewdog-suggester] reported by reviewdog 🐶

Suggested change
let result =
attrs_to_content_items(&[attr], |i, name| (i, name));
let result = attrs_to_content_items(&[attr], |i, name| (i, name));

let err = result.expect_err("expected error for path-qualified #[vm::pymethod]");
let msg = err.to_string();
assert!(
msg.contains("vm::pymethod"),
"error should mention the full path; got: {msg}"
);
assert!(
msg.contains("pymethod"),
"error should mention the unqualified name; got: {msg}"
);
}
}
31 changes: 31 additions & 0 deletions crates/vm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,34 @@ pub use rustpython_literal as literal;
pub mod __exports {
pub use paste;
}

#[cfg(test)]
mod tests {
// Regression test: verify that #[pyclass] and #[pymethod] expand correctly.
// Before the fix in attrs_to_content_items (crates/derive-impl/src/pyclass.rs),
// any inner attribute with a path prefix (e.g. #[vm::pymethod]) caused an
// infinite loop during macro expansion.

#[pyclass(module = false, name = "TestItem")]
#[derive(Debug, PyPayload)]
struct TestItem {
value: i64,
}

#[pyclass]
impl TestItem {
#[pymethod]
fn value(&self) -> i64 {
self.value
}
}

#[test]
fn pyclass_and_pymethod_expand_correctly() {
// Verify the macro-generated code is functional at runtime.
// Before the fix, path-qualified inner attrs (e.g. #[vm::pymethod]) caused
// an infinite loop during macro expansion.
let item = TestItem { value: 42 };
assert_eq!(item.value(), 42);
}
}
Loading