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
- Upgrade Ruff to the latest release; check issues for 'F632 panic Expected CmpOp'
- Reproduce with `ruff check --isolated --select F632 <file>`, minimize, and report upstream
- Exclude F632: `ignore = ["F632"]` under `[tool.ruff.lint]`, and fix `is`/`is not` literal comparisons manually to `==`/`!=`
- 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
- Only call `IsCmpOp::from` inside branches already matched on Is/IsNot
- Prefer returning `Option`/`Result` (like the fix path's `bail!`) over panicking in conversions
- Upgrade Ruff when F632 panics appear; verify with `--isolated --select F632`
- Disable F632 temporarily via `lint.ignore = ["F632"]` if a release regression blocks you
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
- Expected Stmt::ImportFrom
- Expected ScopeKind::Function | ScopeKind::Lambda
- Expected Stmt::Import | Stmt::ImportFrom
- Expected ScopeKind::Function
- extra use-def data should have been retained
AI-assisted analysis of astral-sh/ruff@15f3fe6b15 (2026-09-05).
Data as JSON: /api/errors/b701bcfe65da6792.
Report an issue: GitHub.