Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track conditional deletions in the semantic model #10415

Merged
merged 1 commit into from Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -11,6 +11,13 @@ def f():
print(X)


def f():
global X

if X > 0:
del X


###
# Non-errors.
###
Expand Down
30 changes: 23 additions & 7 deletions crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs
@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Binding, BindingKind, Imported, ScopeKind};
use ruff_python_semantic::{Binding, BindingKind, Imported, ResolvedReference, ScopeKind};
use ruff_text_size::Ranged;

use crate::checkers::ast::Checker;
Expand Down Expand Up @@ -91,13 +91,29 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
if checker.enabled(Rule::GlobalVariableNotAssigned) {
for (name, binding_id) in scope.bindings() {
let binding = checker.semantic.binding(binding_id);
// If the binding is a `global`, then it's a top-level `global` that was never
// assigned in the current scope. If it were assigned, the `global` would be
// shadowed by the assignment.
if binding.kind.is_global() {
diagnostics.push(Diagnostic::new(
pylint::rules::GlobalVariableNotAssigned {
name: (*name).to_string(),
},
binding.range(),
));
// If the binding was conditionally deleted, it will include a reference within
// a `Del` context, but won't be shadowed by a `BindingKind::Deletion`, as in:
// ```python
// if condition:
// del var
// ```
if binding
.references
.iter()
.map(|id| checker.semantic.reference(*id))
.all(ResolvedReference::is_load)
{
diagnostics.push(Diagnostic::new(
pylint::rules::GlobalVariableNotAssigned {
name: (*name).to_string(),
},
binding.range(),
));
}
}
}
}
Expand Down
9 changes: 7 additions & 2 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Expand Up @@ -540,7 +540,11 @@ impl<'a> Visitor<'a> for Checker<'a> {
for name in names {
if let Some((scope_id, binding_id)) = self.semantic.nonlocal(name) {
// Mark the binding as "used".
self.semantic.add_local_reference(binding_id, name.range());
self.semantic.add_local_reference(
binding_id,
ExprContext::Load,
name.range(),
);

// Mark the binding in the enclosing scope as "rebound" in the current
// scope.
Expand Down Expand Up @@ -2113,7 +2117,8 @@ impl<'a> Checker<'a> {
// Mark anything referenced in `__all__` as used.
// TODO(charlie): `range` here should be the range of the name in `__all__`, not
// the range of `__all__` itself.
self.semantic.add_global_reference(binding_id, range);
self.semantic
.add_global_reference(binding_id, ExprContext::Load, range);
} else {
if self.semantic.global_scope().uses_star_imports() {
if self.enabled(Rule::UndefinedLocalWithImportStarUsage) {
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/renamer.rs
Expand Up @@ -255,6 +255,7 @@ impl Renamer {
| BindingKind::ClassDefinition(_)
| BindingKind::FunctionDefinition(_)
| BindingKind::Deletion
| BindingKind::ConditionalDeletion(_)
| BindingKind::UnboundException(_) => {
Some(Edit::range_replacement(target.to_string(), binding.range()))
}
Expand Down
Expand Up @@ -121,8 +121,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
checker
.semantic()
.reference(reference_id)
.context()
.is_runtime()
.in_runtime_context()
})
{
let Some(node_id) = binding.source else {
Expand Down Expand Up @@ -155,8 +154,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
if checker.settings.flake8_type_checking.quote_annotations
&& binding.references().all(|reference_id| {
let reference = checker.semantic().reference(reference_id);
reference.context().is_typing()
|| reference.in_runtime_evaluated_annotation()
reference.in_typing_context() || reference.in_runtime_evaluated_annotation()
})
{
actions
Expand Down Expand Up @@ -268,7 +266,7 @@ fn quote_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding])
.flat_map(|ImportBinding { binding, .. }| {
binding.references.iter().filter_map(|reference_id| {
let reference = checker.semantic().reference(*reference_id);
if reference.context().is_runtime() {
if reference.in_runtime_context() {
Some(quote_annotation(
reference.expression_id()?,
checker.semantic(),
Expand Down
Expand Up @@ -499,7 +499,7 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
.flat_map(|ImportBinding { binding, .. }| {
binding.references.iter().filter_map(|reference_id| {
let reference = checker.semantic().reference(*reference_id);
if reference.context().is_runtime() {
if reference.in_runtime_context() {
Some(quote_annotation(
reference.expression_id()?,
checker.semantic(),
Expand Down
Expand Up @@ -67,6 +67,7 @@ pub(crate) fn non_ascii_name(binding: &Binding, locator: &Locator) -> Option<Dia
| BindingKind::FromImport(_)
| BindingKind::SubmoduleImport(_)
| BindingKind::Deletion
| BindingKind::ConditionalDeletion(_)
| BindingKind::UnboundException(_) => {
return None;
}
Expand Down
34 changes: 30 additions & 4 deletions crates/ruff_python_semantic/src/binding.rs
Expand Up @@ -75,6 +75,11 @@ impl<'a> Binding<'a> {
self.flags.intersects(BindingFlags::GLOBAL)
}

/// Return `true` if this [`Binding`] was deleted.
pub const fn is_deleted(&self) -> bool {
self.flags.intersects(BindingFlags::DELETED)
}

/// Return `true` if this [`Binding`] represents an assignment to `__all__` with an invalid
/// value (e.g., `__all__ = "Foo"`).
pub const fn is_invalid_all_format(&self) -> bool {
Expand Down Expand Up @@ -165,6 +170,7 @@ impl<'a> Binding<'a> {
// Deletions, annotations, `__future__` imports, and builtins are never considered
// redefinitions.
BindingKind::Deletion
| BindingKind::ConditionalDeletion(_)
| BindingKind::Annotation
| BindingKind::FutureImport
| BindingKind::Builtin => {
Expand Down Expand Up @@ -265,14 +271,27 @@ bitflags! {
/// ```
const GLOBAL = 1 << 4;

/// The binding was deleted (i.e., the target of a `del` statement).
///
/// For example, the binding could be `x` in:
/// ```python
/// del x
/// ```
///
/// The semantic model will typically shadow a deleted binding via an additional binding
/// with [`BindingKind::Deletion`]; however, conditional deletions (e.g.,
/// `if condition: del x`) do _not_ generate a shadow binding. This flag is thus used to
/// detect whether a binding was _ever_ deleted, even conditionally.
const DELETED = 1 << 5;

/// The binding represents an export via `__all__`, but the assigned value uses an invalid
/// expression (i.e., a non-container type).
///
/// For example:
/// ```python
/// __all__ = 1
/// ```
const INVALID_ALL_FORMAT = 1 << 5;
const INVALID_ALL_FORMAT = 1 << 6;

/// The binding represents an export via `__all__`, but the assigned value contains an
/// invalid member (i.e., a non-string).
Expand All @@ -281,23 +300,23 @@ bitflags! {
/// ```python
/// __all__ = [1]
/// ```
const INVALID_ALL_OBJECT = 1 << 6;
const INVALID_ALL_OBJECT = 1 << 7;

/// The binding represents a private declaration.
///
/// For example, the binding could be `_T` in:
/// ```python
/// _T = "This is a private variable"
/// ```
const PRIVATE_DECLARATION = 1 << 7;
const PRIVATE_DECLARATION = 1 << 8;

/// The binding represents an unpacked assignment.
///
/// For example, the binding could be `x` in:
/// ```python
/// (x, y) = 1, 2
/// ```
const UNPACKED_ASSIGNMENT = 1 << 8;
const UNPACKED_ASSIGNMENT = 1 << 9;
}
}

Expand Down Expand Up @@ -512,6 +531,13 @@ pub enum BindingKind<'a> {
/// ```
Deletion,

/// A binding for a deletion, like `x` in:
/// ```python
/// if x > 0:
/// del x
/// ```
ConditionalDeletion(BindingId),

/// A binding to bind an exception to a local variable, like `x` in:
/// ```python
/// try:
Expand Down
52 changes: 38 additions & 14 deletions crates/ruff_python_semantic/src/model.rs
Expand Up @@ -5,7 +5,7 @@ use rustc_hash::FxHashMap;

use ruff_python_ast::helpers::from_relative_import;
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
use ruff_python_ast::{self as ast, Expr, Operator, Stmt};
use ruff_python_ast::{self as ast, Expr, ExprContext, Operator, Stmt};
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_text_size::{Ranged, TextRange, TextSize};

Expand Down Expand Up @@ -271,7 +271,7 @@ impl<'a> SemanticModel<'a> {
.get(symbol)
.map_or(true, |binding_id| {
// Treat the deletion of a name as a reference to that name.
self.add_local_reference(binding_id, range);
self.add_local_reference(binding_id, ExprContext::Del, range);
self.bindings[binding_id].is_unbound()
});

Expand All @@ -296,8 +296,9 @@ impl<'a> SemanticModel<'a> {
let reference_id = self.resolved_references.push(
ScopeId::global(),
self.node_id,
name.range,
ExprContext::Load,
self.flags,
name.range,
);
self.bindings[binding_id].references.push(reference_id);

Expand All @@ -308,8 +309,9 @@ impl<'a> SemanticModel<'a> {
let reference_id = self.resolved_references.push(
ScopeId::global(),
self.node_id,
name.range,
ExprContext::Load,
self.flags,
name.range,
);
self.bindings[binding_id].references.push(reference_id);
}
Expand Down Expand Up @@ -365,8 +367,9 @@ impl<'a> SemanticModel<'a> {
let reference_id = self.resolved_references.push(
self.scope_id,
self.node_id,
name.range,
ExprContext::Load,
self.flags,
name.range,
);
self.bindings[binding_id].references.push(reference_id);

Expand All @@ -377,8 +380,9 @@ impl<'a> SemanticModel<'a> {
let reference_id = self.resolved_references.push(
self.scope_id,
self.node_id,
name.range,
ExprContext::Load,
self.flags,
name.range,
);
self.bindings[binding_id].references.push(reference_id);
}
Expand Down Expand Up @@ -426,6 +430,15 @@ impl<'a> SemanticModel<'a> {
return ReadResult::UnboundLocal(binding_id);
}

BindingKind::ConditionalDeletion(binding_id) => {
self.unresolved_references.push(
name.range,
self.exceptions(),
UnresolvedReferenceFlags::empty(),
);
return ReadResult::UnboundLocal(binding_id);
}

// If we hit an unbound exception that shadowed a bound name, resole to the
// bound name. For example, given:
//
Expand All @@ -446,8 +459,9 @@ impl<'a> SemanticModel<'a> {
let reference_id = self.resolved_references.push(
self.scope_id,
self.node_id,
name.range,
ExprContext::Load,
self.flags,
name.range,
);
self.bindings[binding_id].references.push(reference_id);

Expand All @@ -458,8 +472,9 @@ impl<'a> SemanticModel<'a> {
let reference_id = self.resolved_references.push(
self.scope_id,
self.node_id,
name.range,
ExprContext::Load,
self.flags,
name.range,
);
self.bindings[binding_id].references.push(reference_id);
}
Expand Down Expand Up @@ -548,6 +563,7 @@ impl<'a> SemanticModel<'a> {
match self.bindings[binding_id].kind {
BindingKind::Annotation => continue,
BindingKind::Deletion | BindingKind::UnboundException(None) => return None,
BindingKind::ConditionalDeletion(binding_id) => return Some(binding_id),
BindingKind::UnboundException(Some(binding_id)) => return Some(binding_id),
_ => return Some(binding_id),
}
Expand Down Expand Up @@ -1315,18 +1331,28 @@ impl<'a> SemanticModel<'a> {
}

/// Add a reference to the given [`BindingId`] in the local scope.
pub fn add_local_reference(&mut self, binding_id: BindingId, range: TextRange) {
pub fn add_local_reference(
&mut self,
binding_id: BindingId,
ctx: ExprContext,
range: TextRange,
) {
let reference_id =
self.resolved_references
.push(self.scope_id, self.node_id, range, self.flags);
.push(self.scope_id, self.node_id, ctx, self.flags, range);
self.bindings[binding_id].references.push(reference_id);
}

/// Add a reference to the given [`BindingId`] in the global scope.
pub fn add_global_reference(&mut self, binding_id: BindingId, range: TextRange) {
pub fn add_global_reference(
&mut self,
binding_id: BindingId,
ctx: ExprContext,
range: TextRange,
) {
let reference_id =
self.resolved_references
.push(ScopeId::global(), self.node_id, range, self.flags);
.push(ScopeId::global(), self.node_id, ctx, self.flags, range);
self.bindings[binding_id].references.push(reference_id);
}

Expand Down Expand Up @@ -1700,7 +1726,6 @@ bitflags! {
/// only required by the Python interpreter, but by runtime type checkers too.
const RUNTIME_REQUIRED_ANNOTATION = 1 << 2;


/// The model is in a type definition.
///
/// For example, the model could be visiting `int` in:
Expand Down Expand Up @@ -1886,7 +1911,6 @@ bitflags! {
/// ```
const COMPREHENSION_ASSIGNMENT = 1 << 19;


/// The model is in a module / class / function docstring.
///
/// For example, the model could be visiting either the module, class,
Expand Down