{"record":{"id":"64a9ea40ac1f4b60","repo":"astral-sh/ruff","slug":"should-only-ever-pass-a-positive-integer-to-from","errorCode":null,"errorMessage":"Should only ever pass a positive integer to `from_nonnegative_i32`","messagePattern":"Should only ever pass a positive integer to `from_nonnegative_i32`","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/ty_python_semantic/src/subscript.rs","lineNumber":29,"sourceCode":"pub(crate) struct OutOfBoundsError;\n\npub(crate) trait PyIndex<'db> {\n    type Item: 'db;\n\n    fn py_index(\n        self,\n        db: &'db dyn Db,\n        env: &ProgramEnvironment<'db>,\n        index: i32,\n    ) -> Result<Self::Item, OutOfBoundsError>;\n}\n\nfn from_nonnegative_i32(index: i32) -> usize {\n    static_assertions::const_assert!(usize::BITS >= 32);\n    debug_assert!(index >= 0);\n\n    usize::try_from(index)\n        .expect(\"Should only ever pass a positive integer to `from_nonnegative_i32`\")\n}\n\nfn from_negative_i32(index: i32) -> usize {\n    static_assertions::const_assert!(usize::BITS >= 32);\n\n    index.checked_neg().map(from_nonnegative_i32).unwrap_or({\n        // 'checked_neg' only fails for i32::MIN. We cannot\n        // represent -i32::MIN as a i32, but we can represent\n        // it as a usize, since usize is at least 32 bits.\n        from_nonnegative_i32(i32::MAX) + 1\n    })\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]\nenum Position {\n    BeforeStart,\n    AtIndex(usize),\n    AfterEnd,","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/astral-sh/ruff/blob/d1087a4b9e03d253a88703f34e0869ee4b805456/crates/ty_python_semantic/src/subscript.rs#L11-L47","documentation":"from_nonnegative_i32 converts a non-negative i32 subscript index to usize for tuple/sequence element lookup; it carries a debug_assert!(index >= 0) and a release-mode expect because any non-negative i32 converts losslessly into a usize of at least 32 bits. The panic means a negative index reached a path that promises non-negative indices (from_negative_i32 exists for the negative case).","triggerScenarios":"Subscript inference (e.g., tuple element binding for `t[-1]` or literal string indexing) passing a negative index into from_nonnegative_i32 instead of from_negative_i32 - typically a missing sign check at a call site or a newly added subscript path that forgot the negative branch.","commonSituations":"Code that indexes tuples or heterogeneous sequences with negative integer literals; refactors of subscript.rs call sites that drop the sign dispatch.","solutions":["Reduce to a snippet like `t: tuple[int, str]; t[-1]` and file a ty issue with the backtrace","As a contributor: dispatch on the sign (`if index >= 0 { from_nonnegative_i32 } else { from_negative_i32 }`) before converting","Workaround: rewrite the negative literal subscript as a non-negative index while the bug is unfixed"],"exampleFix":"// before\nlet idx = from_nonnegative_i32(index);\n\n// after\nlet idx = if index >= 0 {\n    from_nonnegative_i32(index)\n} else {\n    from_negative_i32(index)\n};","handlingStrategy":"validation","validationCode":"// Caller-side sign dispatch before converting an index:\nlet idx = if index >= 0 { from_nonnegative_i32(index) } else { from_negative_i32(index) };","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always branch on the sign of a subscript index before converting; from_nonnegative_i32 is for the non-negative arm only","When adding subscript paths, add a test with a negative literal index on a tuple","In debug builds the debug_assert fires first - never ignore it in local runs"],"tags":["rust","ty","subscript","panic","negative-index"],"backgroundTag":"negative-index-conversion","analyzedSha":"d1087a4b9e03d253a88703f34e0869ee4b805456","analyzedAt":"2026-08-20T16:33:49.445Z","contentChangedAt":"2026-08-20T16:33:49.445Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}