diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/iteration_over_set.py b/crates/ruff_linter/resources/test/fixtures/pylint/iteration_over_set.py index f8d32e59a572b1..ae8ccb35133903 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/iteration_over_set.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/iteration_over_set.py @@ -60,3 +60,6 @@ for item in {1, 2, 2}: # duplicate literals will be ignored # B033 catches this print(f"I like {item}.") + +for item in {False, 0, 0.0, 0j, True, 1, 1.0}: + print(item) diff --git a/crates/ruff_linter/src/rules/pylint/rules/iteration_over_set.rs b/crates/ruff_linter/src/rules/pylint/rules/iteration_over_set.rs index 0abc2d89b0e5a5..88c8afd56d4766 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/iteration_over_set.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/iteration_over_set.rs @@ -1,8 +1,10 @@ +use rustc_hash::{FxBuildHasher, FxHashSet}; + use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::{comparable::ComparableExpr, Expr}; +use ruff_python_ast::hashable::HashComparableExpr; +use ruff_python_ast::Expr; use ruff_text_size::Ranged; -use rustc_hash::{FxBuildHasher, FxHashSet}; use crate::checkers::ast::Checker; @@ -54,8 +56,7 @@ pub(crate) fn iteration_over_set(checker: &mut Checker, expr: &Expr) { let mut seen_values = FxHashSet::with_capacity_and_hasher(set.len(), FxBuildHasher); for value in set { - let comparable_value = ComparableExpr::from(value); - if !seen_values.insert(comparable_value) { + if !seen_values.insert(HashComparableExpr::from(value)) { // if the set contains a duplicate literal value, early exit. // rule `B033` can catch that. return; diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index a6825b6eb2578c..421c7363d1c938 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -861,8 +861,14 @@ pub struct ExprNumberLiteral<'a> { } #[derive(Debug, PartialEq, Eq, Hash)] -pub struct ExprBoolLiteral<'a> { - value: &'a bool, +pub struct ExprBoolLiteral { + value: bool, +} + +impl From for ExprBoolLiteral { + fn from(value: bool) -> Self { + Self { value } + } } #[derive(Debug, PartialEq, Eq, Hash)] @@ -934,7 +940,7 @@ pub enum ComparableExpr<'a> { StringLiteral(ExprStringLiteral<'a>), BytesLiteral(ExprBytesLiteral<'a>), NumberLiteral(ExprNumberLiteral<'a>), - BoolLiteral(ExprBoolLiteral<'a>), + BoolLiteral(ExprBoolLiteral), NoneLiteral, EllipsisLiteral, Attribute(ExprAttribute<'a>), @@ -1109,7 +1115,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { }) } ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, range: _ }) => { - Self::BoolLiteral(ExprBoolLiteral { value }) + Self::BoolLiteral(ExprBoolLiteral { value: *value }) } ast::Expr::NoneLiteral(_) => Self::NoneLiteral, ast::Expr::EllipsisLiteral(_) => Self::EllipsisLiteral, diff --git a/crates/ruff_python_ast/src/hashable.rs b/crates/ruff_python_ast/src/hashable.rs new file mode 100644 index 00000000000000..2ca7b534ac6ec8 --- /dev/null +++ b/crates/ruff_python_ast/src/hashable.rs @@ -0,0 +1,65 @@ +use std::hash::Hash; + +use crate::{Expr, Number}; + +use crate::comparable::{ComparableExpr, ExprBoolLiteral}; + +/// Wrapper around [`Expr`] that implements [`Hash`] and [`PartialEq`] according to Python +/// semantics: +/// +/// > Values that compare equal (such as 1, 1.0, and True) can be used interchangeably to index the +/// > same dictionary entry. +/// +/// For example, considers `True`, `1`, and `1.0` to be equal, as they hash to the same value +/// in Python, along with `False`, `0`, and `0.0`. +/// +/// See: +pub struct HashComparableExpr<'a>(ComparableExpr<'a>); + +impl Hash for HashComparableExpr<'_> { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl PartialEq for HashComparableExpr<'_> { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for HashComparableExpr<'_> {} + +impl<'a> From<&'a Expr> for HashComparableExpr<'a> { + fn from(expr: &'a Expr) -> Self { + as_bool(expr) + .map(ExprBoolLiteral::from) + .map(ComparableExpr::BoolLiteral) + .map(Self) + .unwrap_or_else(|| Self(ComparableExpr::from(expr))) + } +} + +/// Returns the `bool` value of the given expression, if it has an equivalent hash to `True` or `False`. +fn as_bool(expr: &Expr) -> Option { + let Expr::NumberLiteral(expr) = expr else { + return None; + }; + match &expr.value { + Number::Int(int) => match int.as_u8() { + Some(0) => Some(false), + Some(1) => Some(true), + _ => None, + }, + Number::Float(float) => match float { + 0.0 => Some(false), + 1.0 => Some(true), + _ => None, + }, + Number::Complex { real, imag } => match (real, imag) { + (0.0, 0.0) => Some(false), + (1.0, 0.0) => Some(true), + _ => None, + }, + } +} diff --git a/crates/ruff_python_ast/src/lib.rs b/crates/ruff_python_ast/src/lib.rs index 53c3a72db834b4..b149bdddc8430b 100644 --- a/crates/ruff_python_ast/src/lib.rs +++ b/crates/ruff_python_ast/src/lib.rs @@ -9,6 +9,7 @@ pub use nodes::*; pub mod comparable; pub mod docstrings; mod expression; +pub mod hashable; pub mod helpers; pub mod identifier; mod int;