Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a0b6ed7
Test hover types for __all__ entries
asukaminato0721 Nov 8, 2026
a85faf4
Show referenced symbol types when hovering over __all__ entries
asukaminato0721 Nov 8, 2026
6e33a9c
Use solved export types for __all__ hover
asukaminato0721 Nov 11, 2026
3f68f53
Keep export lookup off identifier type queries
asukaminato0721 Nov 14, 2026
a1af502
Test references in __all__ string entries
asukaminato0721 Nov 8, 2026
a8c59bc
Include __all__ strings in reference results
asukaminato0721 Nov 8, 2026
4222b1f
Resolve __all__ references through checked bindings
asukaminato0721 Nov 11, 2026
e36ec6d
Extract export occurrences once and share binding resolution
asukaminato0721 Nov 14, 2026
80e232b
Test renaming from __all__ string entries
asukaminato0721 Nov 8, 2026
374593f
Allow renaming symbols from __all__ entries
asukaminato0721 Nov 8, 2026
0fd832e
Use retained export occurrences when preparing rename
asukaminato0721 Nov 14, 2026
ba4af25
Test definition lookup from __slots__ entries
asukaminato0721 Nov 8, 2026
6e346dd
Resolve __slots__ strings to their attribute definitions
asukaminato0721 Nov 8, 2026
c7d6e7a
Test hover types for __slots__ entries
asukaminato0721 Nov 8, 2026
f3f2bf2
Show attribute types when hovering over __slots__ entries
asukaminato0721 Nov 8, 2026
cbb8633
Share checked slots entries across definition and hover
asukaminato0721 Nov 14, 2026
bb88834
Test references in __slots__ string entries
asukaminato0721 Nov 8, 2026
3b9bb52
Include __slots__ strings in attribute reference results
asukaminato0721 Nov 8, 2026
4f9121e
Index slots occurrences through semantic attribute resolution
asukaminato0721 Nov 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions pyrefly/lib/alt/answers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ pub struct Index {
/// The call-site range spells the class name, not the definition's own name, so these are
/// gated behind `ReferenceOptions::include_constructor_call_sites`.
pub constructor_references: SmallMap<ModulePath, Vec<(IndexedAttribute, TextRange)>>,
/// Literal slot occurrences, including attributes declared in this module.
pub slot_references: SmallMap<ModulePath, Vec<(IndexedAttribute, TextRange)>>,
/// A map from (child method range) to a list of parent method definitions (ModulePath, parent method).
/// This is used to find reimplementations when doing find-references on parent methods.
pub parent_methods_map: SmallMap<TextRange, Vec<(ModulePath, IndexedAttribute)>>,
Expand Down Expand Up @@ -144,6 +146,8 @@ pub enum AttributeReferenceKind {
/// constructor protocol, as in `Foo()` reaching `Foo.__init__`. Only class construction
/// is recorded this way; calling an instance through its `__call__` is not.
ConstructorCall,
/// A literal name in the owning class's `__slots__` binding.
Slot,
}

/// Visiting a trace store reaches every type recorded in it.
Expand Down Expand Up @@ -1525,8 +1529,33 @@ impl Answers {
}
// `pre_solve` has published every exported slot. From this point on,
// the preallocated solutions table represents a complete result set.
if self.index.is_some() {
for entry in bindings.dunder_slots_entries() {
if let Some(class) = answers_solver.get_idx(entry.class_idx).0.as_ref() {
answers_solver.record_attribute_definition_index(
&answers_solver.instantiate(class),
&entry.name.id,
entry.name.range,
AttributeReferenceKind::Slot,
);
}
}
}
if let Some(index) = &self.index {
let mut index = index.lock();
// Retain export occurrences so unopened reverse dependencies need no AST or answers.
for entry in bindings.dunder_all_entries() {
if let Some(key) = bindings.dunder_all_binding(&entry.id)
&& let Some(IntermediateDefinition::NamedImport(_, module, name, None)) =
key_to_intermediate_definition(bindings, key)
{
index
.externally_defined_variable_references
.entry((module, name))
.or_default()
.push(entry.range);
}
}
// Index bindings with external definitions.
for idx in bindings.keys::<Key>() {
let key = bindings.idx_to_key(idx);
Expand Down Expand Up @@ -1910,6 +1939,18 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
range: _,
docstring_range: _,
} => match reference_kind {
AttributeReferenceKind::Slot => index
.lock()
.slot_references
.entry(cls.module_path().dupe())
.or_default()
.push((
IndexedAttribute {
cls: cls.index(),
name,
},
attribute_reference_range,
)),
AttributeReferenceKind::ConstructorCall => index
.lock()
.constructor_references
Expand Down
30 changes: 2 additions & 28 deletions pyrefly/lib/alt/class/class_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/

use std::iter;

use dupe::Dupe;
use dupe::IterDupedExt;
use itertools::Itertools;
Expand Down Expand Up @@ -69,7 +67,6 @@ use crate::binding::base_class::BaseClassGeneric;
use crate::binding::base_class::BaseClassGenericKind;
use crate::binding::binding::Binding;
use crate::binding::binding::ClassFieldDefinition;
use crate::binding::binding::ExprOrBinding;
use crate::binding::binding::Key;
use crate::binding::binding::KeyAnnotation;
use crate::binding::binding::KeyClassField;
Expand Down Expand Up @@ -710,33 +707,10 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
return ExplicitSlots::Absent;
};
let binding = self.bindings().get::<KeyClassField>(idx);
let ClassFieldDefinition::AssignedInBody { value, .. } = &binding.definition else {
return ExplicitSlots::Unknown;
};
let ExprOrBinding::Expr(expr) = value.as_ref() else {
return ExplicitSlots::Unknown;
};

fn name_of(expr: &Expr) -> Option<Name> {
match expr {
Expr::StringLiteral(s) => Some(Name::new(s.value.to_str())),
_ => None,
}
}
let names: Option<SmallSet<Name>> = match expr {
Expr::Tuple(t) => t.elts.iter().map(name_of).collect(),
Expr::List(l) => l.elts.iter().map(name_of).collect(),
Expr::Dict(d) => d
.items
.iter()
.map(|item| item.key.as_ref().and_then(name_of))
.collect(),
Expr::StringLiteral(s) => Some(iter::once(Name::new(s.value.to_str())).collect()),
_ => return ExplicitSlots::Unknown,
};
let Some(names) = names else {
let Some(entries) = binding.explicit_slots() else {
return ExplicitSlots::Unknown;
};
let names: SmallSet<Name> = entries.into_iter().map(|(name, _)| name).collect();
// A metaclass other than `type` or `ABCMeta` may remove names from the namespace before it
// creates the class.
if cls.module().path().is_interface()
Expand Down
32 changes: 32 additions & 0 deletions pyrefly/lib/binding/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3727,6 +3727,38 @@ pub struct BindingClassField {
pub definition: ClassFieldDefinition,
}

impl BindingClassField {
/// Extract literal slot names and their editable content ranges from the checked field.
pub fn explicit_slots(&self) -> Option<Vec<(Name, Option<TextRange>)>> {
let ClassFieldDefinition::AssignedInBody { value, .. } = &self.definition else {
return None;
};
let ExprOrBinding::Expr(expr) = value.as_ref() else {
return None;
};
let name_of = |expr: &Expr| match expr {
Expr::StringLiteral(literal) => Some((
Name::new(literal.value.to_str()),
literal
.as_single_part_string()
.map(|part| part.content_range()),
)),
_ => None,
};
match expr {
Expr::Tuple(tuple) => tuple.elts.iter().map(name_of).collect(),
Expr::List(list) => list.elts.iter().map(name_of).collect(),
Expr::Dict(dict) => dict
.items
.iter()
.map(|item| item.key.as_ref().and_then(name_of))
.collect(),
Expr::StringLiteral(_) => Some(vec![name_of(expr)?]),
_ => None,
}
}
}

impl DisplayWith<Bindings> for BindingClassField {
fn fmt(&self, f: &mut fmt::Formatter<'_>, ctx: &Bindings) -> fmt::Result {
write!(
Expand Down
65 changes: 63 additions & 2 deletions pyrefly/lib/binding/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,13 @@ table! {
pub struct BindingTable(pub BindingEntry)
}

/// A literal slot occurrence and the class whose field binding declares it.
#[derive(Debug)]
pub struct SlotEntry {
pub name: Identifier,
pub class_idx: Idx<KeyClass>,
}

#[derive(Debug)]
pub struct Bindings {
module_info: ModuleInfo,
Expand All @@ -433,6 +440,9 @@ pub struct Bindings {
annotated_metadata_ranges: Vec<TextRange>,
/// Ranges of the types that were read from docstrings, sorted and non-overlapping.
docstring_type_ranges: Vec<TextRange>,
/// Each entry records an `__all__` name and its range without string delimiters.
dunder_all_entries: Vec<Identifier>,
dunder_slots_entries: Vec<SlotEntry>,
scope_trace: Option<ScopeTrace>,
/// Names `del`eted at module scope; retained outside the LSP `scope_trace`.
module_deletes: SmallSet<Name>,
Expand Down Expand Up @@ -694,6 +704,8 @@ impl Bindings {
annotated_metadata_ranges: Vec::new(),
docstring_type_ranges: Vec::new(),
scope_trace: None,
dunder_all_entries: Vec::new(),
dunder_slots_entries: Vec::new(),
module_deletes: SmallSet::new(),
unset_nodes: UnsetNodes::default(),
global_declarations: SmallSet::new(),
Expand Down Expand Up @@ -951,6 +963,26 @@ impl Bindings {
}
}

pub fn dunder_slots_entries(&self) -> &[SlotEntry] {
&self.dunder_slots_entries
}

pub fn dunder_slots_entry_at(&self, position: TextSize) -> Option<&SlotEntry> {
self.dunder_slots_entries
.iter()
.find(|entry| entry.name.range.contains_inclusive(position))
}

pub fn dunder_all_entries(&self) -> &[Identifier] {
&self.dunder_all_entries
}

/// Return the final local binding for an export, preserving local import aliases.
pub fn dunder_all_binding(&self, name: &Name) -> Option<&Key> {
let export = self.key_to_idx_hashed_opt(Hashed::new(&KeyExport(name.clone())))?;
Some(self.idx_to_key(self.get(export).key_idx()))
}

/// Within the LSP, check if a key exists.
/// It may not exist within `if False:` or `if sys.version == 0:` style code.
pub fn is_valid_key(&self, k: &Key) -> bool {
Expand Down Expand Up @@ -1109,6 +1141,12 @@ impl Bindings {
use_shape_comments_and_stubs: bool,
) -> Self {
let module_range = x.range;
let dunder_all_entries = exports
.dunder_all_names()
.filter_map(|(_, name, content_range)| {
Some(Identifier::new(name.clone(), content_range?))
})
.collect();
let pytest_info = PytestBindingInfo::from_module(&x);
let errors = ErrorCollector::new(module_info.dupe(), error_style);
let shape_comments = if use_shape_comments_and_stubs {
Expand Down Expand Up @@ -1287,7 +1325,7 @@ impl Bindings {
classes: builder.django_relation_classes.into_boxed_slice(),
},
);
Self {
let mut bindings = Self {
module_info,
parse_errors,
binding_errors: builder.errors.finish(),
Expand All @@ -1297,6 +1335,8 @@ impl Bindings {
module_ranges,
annotated_metadata_ranges: merge_ranges(builder.annotated_metadata_ranges),
docstring_type_ranges: merge_ranges(builder.docstring_type_ranges),
dunder_all_entries,
dunder_slots_entries: Vec::new(),
scope_trace: if enable_trace {
Some(scope_trace)
} else {
Expand All @@ -1321,7 +1361,28 @@ impl Bindings {
subsequently_initialized: builder.subsequently_initialized,
promote_ranges: builder.promote_ranges,
functools_wrapper_mutation,
}
};
bindings.dunder_slots_entries = bindings
.keys::<KeyClassField>()
.flat_map(|idx| {
let field = bindings.get(idx);
if field.name != *dunder::SLOTS {
return Vec::new();
}
field
.explicit_slots()
.into_iter()
.flatten()
.filter_map(|(name, range)| {
Some(SlotEntry {
name: Identifier::new(name, range?),
class_idx: field.class_idx,
})
})
.collect()
})
.collect();
bindings
}

fn should_emit_semantic_syntax_error(error: &SemanticSyntaxError) -> bool {
Expand Down
Loading
Loading