astral-sh/ruff · error

Expected CmpOp::Is | CmpOp::IsNot

Error message

Expected CmpOp::Is | CmpOp::IsNot

What it means

Internal panic in the `From<&CmpOp> for IsCmpOp` conversion in Ruff's pyflakes F632 rule (invalid literal `is` comparisons). The conversion is only called after the rule has matched `CmpOp::Is | CmpOp::IsNot`, so any other operator is treated as unreachable. Note the fix path itself deliberately avoids this by using `bail!` for unexpected ops; the `From` impl still panics, so a desync between matching and conversion crashes instead of erroring gracefully.

Source

Thrown at crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs:131

                    bail!("Failed to fix invalid comparison due to missing op")
                }
            });
        }
    }
}

#[derive(Debug, PartialEq, Eq, Copy, Clone)]
enum IsCmpOp {
    Is,
    IsNot,
}

impl From<&CmpOp> for IsCmpOp {
    fn from(cmp_op: &CmpOp) -> Self {
        match cmp_op {
            CmpOp::Is => IsCmpOp::Is,
            CmpOp::IsNot => IsCmpOp::IsNot,
            _ => panic!("Expected CmpOp::Is | CmpOp::IsNot"),
        }
    }
}

/// Extract all [`CmpOp`] operators from an expression snippet, with appropriate ranges.
///
/// This method iterates over the token stream and re-identifies [`CmpOp`] nodes, annotating them
/// with valid ranges.
fn locate_cmp_ops(range: TextRange, tokens: &Tokens) -> Vec<LocatedCmpOp> {
    let mut tok_iter = tokens
        .in_range(range)
        .iter()
        .filter(|token| !token.kind().is_trivia())
        .peekable();

    let mut ops: Vec<LocatedCmpOp> = vec![];

    // Track the nesting level.

View on GitHub (pinned to 15f3fe6b15)

Solutions

  1. Upgrade Ruff to the latest release; check issues for 'F632 panic Expected CmpOp'
  2. Reproduce with `ruff check --isolated --select F632 <file>`, minimize, and report upstream
  3. Exclude F632: `ignore = ["F632"]` under `[tool.ruff.lint]`, and fix `is`/`is not` literal comparisons manually to `==`/`!=`
  4. If editing the rule, prefer the graceful pattern already used in the fix path (`bail!("Failed to fix ...")) over panicking in `From`

Example fix

// before (Rust, in invalid_literal_comparisons.rs)
_ => panic!("Expected CmpOp::Is | CmpOp::IsNot"),

// after (graceful fallback instead of panic)
CmpOp::Eq | CmpOp::NotEq => return IsCmpOp::Is, // or handle via Option/Result
_ => panic!("Expected CmpOp::Is | CmpOp::IsNot"), // unreachable by construction
Defensive patterns

Strategy: type-guard

Validate before calling

// Rust: guard before conversion, mirroring the rule's own match
if matches!(op, ast::CmpOp::Is | ast::CmpOp::IsNot) {
    let converted: IsCmpOp = op.into();
}

Type guard

fn is_identity_cmp(op: &CmpOp) -> bool {
    matches!(op, CmpOp::Is | CmpOp::IsNot)
}

Try / catch

// panic guards around the rule execution
let ok = std::panic::catch_unwind(|| ruff_check_f632(path)).is_ok();

Prevention

When it happens

Trigger: Linting code like `1 is 1` or `[] is []` (F632) where the comparison operator passed to `IsCmpOp::from` is not `Is`/`IsNot`. In practice only via a Ruff regression in operator extraction (`locate_cmp_ops`) or refactoring that calls `.into()` outside the guarded `matches!` branch.

Common situations: Running Ruff on files with chained/malformed comparisons after a parser change; contributor builds where `invalid_literal_comparisons` was refactored; old Ruff versions with a known operator-extraction bug.

Related errors


AI-assisted analysis of astral-sh/ruff@15f3fe6b15 (2026-09-05). Data as JSON: /api/errors/b701bcfe65da6792. Report an issue: GitHub.