Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions pyrefly/lib/binding/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,10 @@ impl Bindings {
);
}

if let Some(exported_names) = exports.get_explicit_dunder_all_names_iter() {
builder.record_used_imports_from_dunder_all_names(exported_names);
}

let unused_imports = builder.scopes.collect_module_unused_imports();
builder.record_unused_imports(unused_imports);
let scope_trace = builder.scopes.finish();
Expand Down Expand Up @@ -684,6 +688,17 @@ impl<'a> BindingsBuilder<'a> {
self.unused_variables.extend(unused);
}

pub fn record_used_imports_from_dunder_all_names<T>(&mut self, dunder_all_names: T)
where
T: Iterator<Item = &'a Name>,
{
for name in dunder_all_names {
if self.scopes.has_import_name(&name) {
self.scopes.mark_import_used(&name);
}
}
}

pub(crate) fn with_await_context<R>(
&mut self,
ctx: AwaitContext,
Expand Down
9 changes: 9 additions & 0 deletions pyrefly/lib/binding/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,15 @@ impl Scopes {
ScopeTrace(b)
}

pub fn has_import_name(&self, name: &Name) -> bool {
let module_scope = self.scopes.first();

match module_scope.scope.kind {
ScopeKind::Module => module_scope.scope.imports.contains_key(name),
_ => false,
}
}

pub fn collect_module_unused_imports(&self) -> Vec<UnusedImport> {
let module_scope = self.scopes.first();
if !matches!(module_scope.scope.kind, ScopeKind::Module) {
Expand Down
18 changes: 18 additions & 0 deletions pyrefly/lib/export/exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,24 @@ impl Exports {
.contains(name)
}

/// Return an iterator with entries in `__all__` that are user-defined or None if `__all__` was not present.
pub fn get_explicit_dunder_all_names_iter(&self) -> Option<impl Iterator<Item = &Name>> {
match self.0.definitions.dunder_all.kind {
DunderAllKind::Specified => Some(
self.0
.definitions
.dunder_all
.entries
.iter()
.filter_map(|entry| match entry {
DunderAllEntry::Name(_, name) => Some(name),
_ => None,
}),
),
_ => None,
}
}

/// Returns entries in `__all__` that don't exist in the module's definitions.
/// Only validates explicitly user-defined `__all__` entries, not synthesized ones.
/// Returns a vector of (range, name) tuples for invalid entries.
Expand Down
26 changes: 26 additions & 0 deletions pyrefly/lib/test/lsp/lsp_interaction/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,32 @@ fn test_unused_from_import_diagnostic() {
interaction.shutdown().unwrap();
}

#[test]
fn test_diagnostic_import_used_in_all() {
let test_files_root = get_test_files_root();
let mut interaction = LspInteraction::new();
interaction.set_root(test_files_root.path().to_path_buf());
interaction
.initialize(InitializeSettings {
configuration: Some(Some(json!([
{"pyrefly": {"displayTypeErrors": "force-on"}}
]))),
..Default::default()
})
.unwrap();

interaction.client.did_open("unused_import_all/__init__.py");
interaction
.client
.diagnostic("unused_import_all/__init__.py")
.expect_response(json!({
"items": [],
"kind": "full"
}))
.unwrap();
interaction.shutdown().unwrap();
}

#[test]
fn test_unused_variable_diagnostic() {
let test_files_root = get_test_files_root();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from foo import Foo, Bar as Baz

__all__ = ["Foo", "Baz"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.


class Foo: ...


class Bar: ...