{"record":{"id":"fe0db3029f720c99","repo":"astral-sh/ruff","slug":"anchor-should-not-be-nodeindex-none","errorCode":null,"errorMessage":"anchor should not be NodeIndex::NONE","messagePattern":"anchor should not be NodeIndex::NONE","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/ty_python_semantic/src/types/class.rs","lineNumber":126,"sourceCode":"    anchor: DynamicClassHeaderAnchor<'db>,\n) -> TextRange {\n    let module = parsed_module(db, scope.python_file(db)).load(db);\n    match anchor {\n        DynamicClassHeaderAnchor::Definition(definition) => definition\n            .kind(db)\n            .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","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/astral-sh/ruff/blob/15f3fe6b15a5f00172f34b0f542f8ea277f5a586/crates/ty_python_semantic/src/types/class.rs#L108-L144","documentation":"For dangling dynamic-class calls (a call with no assignment), ty stores a `DynamicClassScopeOffset` relative to the enclosing scope's AST node index. Resolving the header range converts the scope's node index to a raw u32; this expect fires if that index is the `NodeIndex::NONE` sentinel, meaning the scope's node has no usable index in the parsed module. Without it the offset arithmetic `NodeIndex::from(anchor_u32 + offset)` cannot be trusted.","triggerScenarios":"Resolving a `DynamicClassScopeOffset` anchor where `scope.node(db).node_index()` is missing or is the NONE sentinel - e.g. a synthetic scope, or a scope whose node lives in a different module than the one loaded via `scope.python_file(db)`.","commonSituations":"Modifying how dangling calls get anchored (new scope kinds, lambda/comprehension bodies); LSP incremental edits where the scope's node index shifts after reparse but the anchor was not recomputed; a refactor that lets scopes without AST nodes own dynamic classes.","solutions":["At anchor construction, only build `DynamicClassAnchor::ScopeOffset` for scopes whose `node_index()` is Some and not the NONE sentinel; otherwise fall back to storing the absolute range.","Check that the module used for resolution is the scope's own `python_file`, and that the anchor has a Salsa dependency on that parse.","Add an mdtest with a dangling call in the same scope shape that panicked and run the ty_python_semantic mdtests.","If reachable on stock ty, file an issue with the reproducer."],"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":["Treat any dynamic-class range panic as a ty bug: reduce the input to the smallest file that still panics before reporting.","In editor integrations, wrap per-file checking so one file's panic does not take down the LSP session.","Keep ty updated; anchoring fixes land regularly."],"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-14T05:17:10.506Z"}