Summary
Query::get_attributes panics when a class named classproperty or cached_classproperty has no type arguments. The match arm keys on the class name alone, then assumes type arguments are present:
|
Type::ClassType(c) |
|
if c.name() == "classproperty" || c.name() == "cached_classproperty" => |
|
{ |
|
let result_ty = c.targs().as_slice().first().unwrap(); |
|
(Some(String::from("property")), result_ty) |
Type::ClassType(c)
if c.name() == "classproperty" || c.name() == "cached_classproperty" =>
{
let result_ty = c.targs().as_slice().first().unwrap();
(Some(String::from("property")), result_ty)
}
A plain, non-generic classproperty — a common Python idiom for combining classmethod and property — reaches this arm with empty targs, and .first().unwrap() panics.
thread '...' panicked at pyrefly/lib/query.rs:1147:66:
called `Option::unwrap()` on a `None` value
Reproduction
Verified against main @ d5ce6d8. Python input:
class classproperty:
def __init__(self, f):
self.f = f
def __get__(self, obj, owner):
return self.f(owner)
class Config:
@classproperty
def name(cls) -> str:
return "cfg"
As a test in pyrefly/lib/test/query.rs:
#[test]
fn repro_non_generic_classproperty_panics() {
let tdir = TempDir::new().unwrap();
let file_path = tdir.path().join("main.py");
let code = r#"<the Python above>"#;
fs_anyhow::write(&file_path, code).unwrap();
let query = create_query();
let module_name = ModuleName::from_str("main");
let path = ModulePath::filesystem(file_path.clone());
let errors = query.add_files(vec![(module_name, path.clone())]);
assert!(errors.is_empty(), "unexpected errors: {errors:?}");
// panics at query.rs:1147
let _ = query.get_attributes(module_name, path, "Config");
}
Control: changing the class to class classproperty[T, R]: makes the same test pass, isolating the failure to the no-type-arguments case. pyrefly check on the same file reports 0 errors — this affects the Query::get_attributes API rather than the CLI check path.
Note on verification
Found while evaluating Rust verification tooling. Working this through the midas-lex tool identifies the spec owner here as a precondition: indexing element 0 requires the slice to be non-empty, and matching on the class name does not establish that. Modelling the same obligation in Verus reports precondition not satisfied on the indexing as written; the identical body verifies once targs.len() > 0 is available, or with an explicit length check (3 verified, 1 errors).
Suggested fix
Handle the no-type-arguments case rather than indexing unconditionally — for example falling back to ty so the class is treated as a plain attribute:
Type::ClassType(c)
if c.name() == "classproperty" || c.name() == "cached_classproperty" =>
{
match c.targs().as_slice().first() {
Some(result_ty) => (Some(String::from("property")), result_ty),
None => (Some(String::from("property")), ty),
}
}
Happy to send a PR if that's welcome — noting CONTRIBUTING asks to discuss approach on an issue first.
Summary
Query::get_attributespanics when a class namedclasspropertyorcached_classpropertyhas no type arguments. The match arm keys on the class name alone, then assumes type arguments are present:pyrefly/pyrefly/lib/query.rs
Lines 1144 to 1148 in d5ce6d8
A plain, non-generic
classproperty— a common Python idiom for combiningclassmethodandproperty— reaches this arm with emptytargs, and.first().unwrap()panics.Reproduction
Verified against
main@d5ce6d8. Python input:As a test in
pyrefly/lib/test/query.rs:Control: changing the class to
class classproperty[T, R]:makes the same test pass, isolating the failure to the no-type-arguments case.pyrefly checkon the same file reports 0 errors — this affects theQuery::get_attributesAPI rather than the CLI check path.Note on verification
Found while evaluating Rust verification tooling. Working this through the midas-lex tool identifies the spec owner here as a precondition: indexing element 0 requires the slice to be non-empty, and matching on the class name does not establish that. Modelling the same obligation in Verus reports
precondition not satisfiedon the indexing as written; the identical body verifies oncetargs.len() > 0is available, or with an explicit length check (3 verified, 1 errors).Suggested fix
Handle the no-type-arguments case rather than indexing unconditionally — for example falling back to
tyso the class is treated as a plain attribute:Happy to send a PR if that's welcome — noting CONTRIBUTING asks to discuss approach on an issue first.