{"record":{"id":"b701bcfe65da6792","repo":"astral-sh/ruff","slug":"expected-cmpop-is-cmpop-isnot","errorCode":null,"errorMessage":"Expected CmpOp::Is | CmpOp::IsNot","messagePattern":"Expected CmpOp::Is \\| CmpOp::IsNot","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs","lineNumber":131,"sourceCode":"                    bail!(\"Failed to fix invalid comparison due to missing op\")\n                }\n            });\n        }\n    }\n}\n\n#[derive(Debug, PartialEq, Eq, Copy, Clone)]\nenum IsCmpOp {\n    Is,\n    IsNot,\n}\n\nimpl From<&CmpOp> for IsCmpOp {\n    fn from(cmp_op: &CmpOp) -> Self {\n        match cmp_op {\n            CmpOp::Is => IsCmpOp::Is,\n            CmpOp::IsNot => IsCmpOp::IsNot,\n            _ => panic!(\"Expected CmpOp::Is | CmpOp::IsNot\"),\n        }\n    }\n}\n\n/// Extract all [`CmpOp`] operators from an expression snippet, with appropriate ranges.\n///\n/// This method iterates over the token stream and re-identifies [`CmpOp`] nodes, annotating them\n/// with valid ranges.\nfn locate_cmp_ops(range: TextRange, tokens: &Tokens) -> Vec<LocatedCmpOp> {\n    let mut tok_iter = tokens\n        .in_range(range)\n        .iter()\n        .filter(|token| !token.kind().is_trivia())\n        .peekable();\n\n    let mut ops: Vec<LocatedCmpOp> = vec![];\n\n    // Track the nesting level.","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/astral-sh/ruff/blob/15f3fe6b15a5f00172f34b0f542f8ea277f5a586/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs#L113-L149","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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`"],"exampleFix":"// before (Rust, in invalid_literal_comparisons.rs)\n_ => panic!(\"Expected CmpOp::Is | CmpOp::IsNot\"),\n\n// after (graceful fallback instead of panic)\nCmpOp::Eq | CmpOp::NotEq => return IsCmpOp::Is, // or handle via Option/Result\n_ => panic!(\"Expected CmpOp::Is | CmpOp::IsNot\"), // unreachable by construction","handlingStrategy":"type-guard","validationCode":"// Rust: guard before conversion, mirroring the rule's own match\nif matches!(op, ast::CmpOp::Is | ast::CmpOp::IsNot) {\n    let converted: IsCmpOp = op.into();\n}","typeGuard":"fn is_identity_cmp(op: &CmpOp) -> bool {\n    matches!(op, CmpOp::Is | CmpOp::IsNot)\n}","tryCatchPattern":"// panic guards around the rule execution\nlet ok = std::panic::catch_unwind(|| ruff_check_f632(path)).is_ok();","preventionTips":["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"],"tags":["rust","panic","ruff","pyflakes","f632","internal-invariant"],"backgroundTag":"linter-internal-panic","analyzedSha":"15f3fe6b15a5f00172f34b0f542f8ea277f5a586","analyzedAt":"2026-09-05T10:32:37.492Z","contentChangedAt":"2026-09-05T10:32:37.492Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}