{"record":{"id":"61741637010e8b37","repo":"astral-sh/ruff","slug":"string-annotation-offset-should-point-to-exprstrin","errorCode":null,"errorMessage":"string annotation offset should point to ExprStringLiteral","messagePattern":"string annotation offset should point to ExprStringLiteral","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/ty_python_semantic/src/types/class.rs","lineNumber":132,"sourceCode":"            .value(&module)\n            .expect(\"dynamic class definitions should only be used for assignments\")\n            .range(),\n        DynamicClassHeaderAnchor::ScopeOffset(offset) => {\n            let (offset, relative_range) = match offset {\n                DynamicClassScopeOffset::Node(offset) => (offset, None),\n                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)]","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/astral-sh/ruff/blob/15f3fe6b15a5f00172f34b0f542f8ea277f5a586/crates/ty_python_semantic/src/types/class.rs#L114-L150","documentation":"A dynamic class created inside a string (forward-reference) annotation stores the outer string-literal node offset plus a range inside the parsed annotation text (`DynamicClassScopeOffset::StringAnnotation`). Resolution indexes the module with the stored offset and downcasts the node to `ast::ExprStringLiteral`; this expect fires when the node at that index is not a string literal.","triggerScenarios":"Resolving a StringAnnotation anchor whose stored offset no longer lands on the string literal - the module AST changed shape (reparse, edit) while the anchor survived, or the offset was computed against a different scope's node index than the one used at resolution time.","commonSituations":"LSP server sessions where an edit reparses the file but the cached anchor is not invalidated (missing dependency on `parsed_module`); contributions adding new string-annotation contexts (PEP 649 deferred annotations, `__annotations__` strings, nested quoted annotations).","solutions":["Ensure the query that stores the offset reads `parsed_module` through Salsa for the same file, so edits invalidate the anchor.","Verify the offset is always computed from `scope.node(db).node_index()` of the same scope passed to `dynamic_class_header_range`.","Add an mdtest with the exact quoted annotation that triggered it and run `cargo nextest run -p ty_python_semantic --test mdtest`.","Report with the reproducer if it occurs on an unmodified build."],"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":["When reporting, include the exact quoted annotation string - reproducers hinge on it.","Avoid exotic nesting of dynamic-class calls inside string annotations while a fix is pending (unquote the annotation).","Re-test after every ty upgrade; string-annotation anchoring is new code."],"tags":["rust","panic","expect","ty","dynamic-class","string-annotation","node-index"],"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-14T05:17:10.506Z"}