{"record":{"id":"690de27d2a5e057e","repo":"astral-sh/ruff","slug":"dynamic-class-definitions-should-only-be-used-for","errorCode":null,"errorMessage":"dynamic class definitions should only be used for assignments","messagePattern":"dynamic class definitions should only be used for assignments","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/ty_python_semantic/src/types/class.rs","lineNumber":114,"sourceCode":"    StringAnnotation { offset: u32, range: TextRange },\n}\n\n/// Returns the source range of a call that creates a dynamic class.\n///\n/// ```python\n/// Color = Enum(\"Color\", \"RED GREEN\")\n/// #       ^^^^^^^^^^^^^^^^^^^^^^^^^^\n/// ```\nfn dynamic_class_header_range<'db>(\n    db: &'db dyn Db,\n    scope: ScopeId<'db>,\n    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()","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/astral-sh/ruff/blob/15f3fe6b15a5f00172f34b0f542f8ea277f5a586/crates/ty_python_semantic/src/types/class.rs#L96-L132","documentation":"ty models 'dynamic classes' created by factory calls assigned to a name, e.g. `Color = Enum('Color', 'RED GREEN')`. `dynamic_class_header_range` maps such a class back to its source range for diagnostics. The `Definition` anchor path assumes the stored `Definition` is an assignment: `definition.kind(db).value(&module)` must return the right-hand-side expression. The expect fires when a `DynamicClassHeaderAnchor::Definition` was built for a definition that has no value node in the loaded module (not an assignment, or a definition resolved against a different parse).","triggerScenarios":"Calling `header_range`/`header_span` on a dynamic class whose anchor is `DynamicClassAnchor::Definition` (anchors are built in dynamic_literal.rs, enum_literal.rs, named_tuple.rs, typed_dict.rs) while `definition.kind(db).value(&module)` returns None - i.e. the definition kind is not an assignment with a value expression in that module.","commonSituations":"Contributing to ty and extending dynamic-class detection (new factory functions, walrus targets, rebinding) so anchors get built for non-assignment definitions; refactors that change `DefinitionKind` handling; an anchor kept alive without a Salsa dependency on the file's parse so it is resolved against a stale/different AST.","solutions":["At every `DynamicClassHeaderAnchor::Definition(definition)` construction site (dynamic_literal.rs:248, enum_literal.rs:175, named_tuple.rs:219, typed_dict.rs:949), verify `definition.kind(db).value(&module)` is Some first; if not, use the ScopeOffset anchor or skip the range.","Reproduce with a minimal Python file (variations of `X = Enum('X', 'A B')`) and run `INSTA_FORCE_PASS=1 cargo nextest run -p ty_python_semantic`.","Confirm the module passed to `.value()` is parsed from the same file the definition belongs to, not merely the scope's file.","If it reproduces on an unmodified ty build, file an issue at astral-sh/ruff with the input file and backtrace."],"exampleFix":"// before\nlet anchor = DynamicClassHeaderAnchor::Definition(definition);\n\n// after: only anchor assignments that have a value node in this module\nlet anchor = match definition.kind(db).value(&module) {\n    Some(_) => DynamicClassHeaderAnchor::Definition(definition),\n    None => return None,\n};","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// embedding ty: keep span computation from killing the host\nlet range = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n    dynamic_class_header_range(db, scope, anchor)\n}))\n.unwrap_or_else(|_| file.range()); // fall back to the whole file","preventionTips":["Pin the ty/ruff version you validate against; do not mix builds.","Embedders: install a panic hook that logs and quarantine the file rather than crashing the process.","If a stock build panics, capture RUST_BACKTRACE=1 output and file it at github.com/astral-sh/ruff/issues with the input file."],"tags":["rust","panic","expect","ty","dynamic-class","source-span","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"}