Skip to content

panic: Option::unwrap() on None in Query::get_attributes for a non-generic classproperty class #4453

Description

@renz011tzar

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:

pyrefly/pyrefly/lib/query.rs

Lines 1144 to 1148 in d5ce6d8

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.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions