{"record":{"id":"46450845f22de36d","repo":"astral-sh/ruff","slug":"scope-offset-should-point-to-exprcall","errorCode":null,"errorMessage":"scope offset should point to ExprCall","messagePattern":"scope offset should point to ExprCall","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/ty_python_semantic/src/types/class.rs","lineNumber":138,"sourceCode":"                DynamicClassScopeOffset::StringAnnotation { offset, range } => {\n                    (offset, Some(range))\n                }\n            };\n            let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0));\n            let anchor_u32 = scope_anchor\n                .as_u32()\n                .expect(\"anchor should not be NodeIndex::NONE\");\n            let absolute_index = NodeIndex::from(anchor_u32 + offset);\n            if let Some(relative_range) = relative_range {\n                let string: &ast::ExprStringLiteral = module\n                    .get_by_index(absolute_index)\n                    .try_into()\n                    .expect(\"string annotation offset should point to ExprStringLiteral\");\n                return relative_range + string.start();\n            }\n            let node: &ast::ExprCall = module\n                .get_by_index(absolute_index)\n                .try_into()\n                .expect(\"scope offset should point to ExprCall\");\n            node.range()\n        }\n    }\n}\n\nbitflags::bitflags! {\n    /// Properties shared by all instances of a class.\n    ///\n    /// This combines properties derived from the MRO into the existing class-classification\n    /// query, avoiding a separate cached query for each property.\n    #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]\n    pub(super) struct ClassInstanceFlags: u8 {\n        /// The class is, or inherits from, a `TypedDict` specification.\n        const TYPED_DICT = 1 << 0;\n        /// The class directly or indirectly inherits from an explicit `Any` base.\n        const INHERITS_FROM_EXPLICIT_ANY = 1 << 1;\n        /// The class may define or inherit a custom `__getattribute__` method.","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/astral-sh/ruff/blob/15f3fe6b15a5f00172f34b0f542f8ea277f5a586/crates/ty_python_semantic/src/types/class.rs#L120-L156","documentation":"The non-annotation counterpart of the string-literal check: for a dangling dynamic-class call in a scope body, the stored scope-relative offset must land on the `ast::ExprCall` node (the `make_class`-style call). The `try_into().expect(...)` fires when the node resolved by `NodeIndex::from(anchor_u32 + offset)` is any other expression kind.","triggerScenarios":"Resolving a `DynamicClassScopeOffset::Node(offset)` anchor where offset + scope node index points at a non-call node: index drift after a reparse, an offset computed from the wrong base node, or a scope whose node index changed between anchor creation and resolution.","commonSituations":"Editing ty's dangling-call detection to anchor calls in new positions (decorators, subscripts, nested calls); incremental LSP edits where the AST shifts under a cached anchor; test fixtures generated from a differently-shaped module.","solutions":["Recompute or invalidate offsets together with `parsed_module` for the file so index arithmetic always runs on the parse the offset came from.","Write a failing mdtest with the minimal dangling call (e.g. a bare `Enum('X', 'A')` expression statement) and fix anchoring until it passes.","Double-check the base used at creation (`scope.node(db).node_index()`) matches the base used in `dynamic_class_header_range`.","Report upstream if reachable without source changes."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"let range = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n    dynamic_class_header_range(db, scope, anchor)\n}))\n.unwrap_or_else(|_| file.range());","preventionTips":["Assign the factory call to a name (`C = Enum(...)`) instead of leaving it dangling; assigned forms use a different, simpler anchor path.","Reduce the repro to the smallest dangling call before reporting.","Embedders: catch_unwind plus a whole-file fallback span keeps diagnostics usable."],"tags":["rust","panic","expect","ty","dynamic-class","node-index","invariant"],"backgroundTag":"rust-expect-panic","analyzedSha":"15f3fe6b15a5f00172f34b0f542f8ea277f5a586","analyzedAt":"2026-08-20T16:33:49.445Z","contentChangedAt":"2026-08-20T16:33:49.445Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}