astral-sh/ruff · error
scope offset should point to ExprCall
Error message
scope offset should point to ExprCall
What it means
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.
Source
Thrown at crates/ty_python_semantic/src/types/class.rs:138
DynamicClassScopeOffset::StringAnnotation { offset, range } => {
(offset, Some(range))
}
};
let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0));
let anchor_u32 = scope_anchor
.as_u32()
.expect("anchor should not be NodeIndex::NONE");
let absolute_index = NodeIndex::from(anchor_u32 + offset);
if let Some(relative_range) = relative_range {
let string: &ast::ExprStringLiteral = module
.get_by_index(absolute_index)
.try_into()
.expect("string annotation offset should point to ExprStringLiteral");
return relative_range + string.start();
}
let node: &ast::ExprCall = module
.get_by_index(absolute_index)
.try_into()
.expect("scope offset should point to ExprCall");
node.range()
}
}
}
bitflags::bitflags! {
/// Properties shared by all instances of a class.
///
/// This combines properties derived from the MRO into the existing class-classification
/// query, avoiding a separate cached query for each property.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
pub(super) struct ClassInstanceFlags: u8 {
/// The class is, or inherits from, a `TypedDict` specification.
const TYPED_DICT = 1 << 0;
/// The class directly or indirectly inherits from an explicit `Any` base.
const INHERITS_FROM_EXPLICIT_ANY = 1 << 1;
/// The class may define or inherit a custom `__getattribute__` method.View on GitHub (pinned to 15f3fe6b15)
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.
Defensive patterns
Strategy: try-catch
Try / catch
let range = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
dynamic_class_header_range(db, scope, anchor)
}))
.unwrap_or_else(|_| file.range()); Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- anchor should not be NodeIndex::NONE
- dynamic class definitions should only be used for assignment
- string annotation offset should point to ExprStringLiteral
- DynamicClassAnchor::Definition should only be used for assig
- Definition value should be a call expression
AI-assisted analysis of astral-sh/ruff@15f3fe6b15 (2026-08-20).
Data as JSON: /api/errors/46450845f22de36d.
Report an issue: GitHub.