Skip to content

Conversation

danparizher
Copy link
Contributor

Summary

This PR implements RUF065 (BinaryOperatorIdentity) that detects binary operations between a value and itself and suggests simplifications to their known identities. This rule is inspired by similar functionality in Sourcery and contributes to issue #11060.

What it does

The rule identifies binary operations where both operands are identical and can be simplified to known mathematical identities:

  • x | xx (bitwise OR identity)
  • x & xx (bitwise AND identity)
  • x ^ x0 (bitwise XOR identity)
  • x - x0 (subtraction identity)
  • x / x1 (division identity)
  • x // x1 (floor division identity)
  • x % x0 (modulo identity)

@ntBre, @MichaReiser, I wanted to draft this PR as a proof of concept of what this may look like as a custom RUF rule implementation. Please critique and comment as you see fit! I'm sure there are many details to discuss in terms of where we want the rule to live, semantics, opinionatedness, etc. Thanks!

Copy link
Contributor

github-actions bot commented Jul 24, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+104 -0 violations, +0 -0 fixes in 5 projects; 50 projects unchanged)

ibis-project/ibis (+12 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview

+ ibis/backends/tests/test_array.py:668:24: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ ibis/backends/tests/test_generic.py:332:23: RUF065 Binary operation `&` between a value and itself can be simplified to `t.bool_col`
+ ibis/backends/tests/test_generic.py:333:24: RUF065 Binary operation `&` between a value and itself can be simplified to `df.bool_col`
+ ibis/backends/tests/test_generic.py:342:23: RUF065 Binary operation `^` between a value and itself can be simplified to `0`
+ ibis/backends/tests/test_generic.py:343:24: RUF065 Binary operation `^` between a value and itself can be simplified to `0`
+ ibis/expr/operations/tests/test_rewrites.py:100:10: RUF065 Binary operation `/` between a value and itself can be simplified to `1`
+ ibis/expr/operations/tests/test_rewrites.py:101:10: RUF065 Binary operation `/` between a value and itself can be simplified to `1`
+ ibis/expr/operations/tests/test_rewrites.py:102:17: RUF065 Binary operation `/` between a value and itself can be simplified to `1`
+ ibis/expr/operations/tests/test_rewrites.py:103:17: RUF065 Binary operation `/` between a value and itself can be simplified to `1`
+ ibis/expr/operations/tests/test_rewrites.py:104:19: RUF065 Binary operation `/` between a value and itself can be simplified to `1`
... 2 additional changes omitted for project

pandas-dev/pandas (+62 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview

+ asv_bench/benchmarks/arithmetic.py:378:27: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ asv_bench/benchmarks/arithmetic.py:382:9: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_datetime64.py:1045:20: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_datetime64.py:1060:20: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_datetime64.py:2157:20: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_datetime64.py:2177:18: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_datetime64.py:2180:18: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_numeric.py:1446:31: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_numeric.py:1447:21: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_numeric.py:530:18: RUF065 Binary operation `/` between a value and itself can be simplified to `1`
+ pandas/tests/arithmetic/test_numeric.py:825:32: RUF065 Binary operation `/` between a value and itself can be simplified to `1`
+ pandas/tests/arithmetic/test_object.py:325:13: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_period.py:1328:20: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_timedelta64.py:374:18: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_timedelta64.py:431:18: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_timedelta64.py:447:18: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_timedelta64.py:576:20: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arithmetic/test_timedelta64.py:587:15: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/arrays/integer/test_arithmetic.py:330:45: RUF065 Binary operation `&` between a value and itself can be simplified to `4`
+ pandas/tests/arrays/integer/test_arithmetic.py:334:45: RUF065 Binary operation `^` between a value and itself can be simplified to `0`
+ pandas/tests/arrays/test_datetimelike.py:823:59: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/dtypes/test_missing.py:206:53: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/extension/base/dim2.py:287:57: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/extension/test_arrow.py:1258:46: RUF065 Binary operation `&` between a value and itself can be simplified to `4`
+ pandas/tests/extension/test_arrow.py:1262:46: RUF065 Binary operation `^` between a value and itself can be simplified to `0`
+ pandas/tests/frame/methods/test_diff.py:64:20: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/frame/methods/test_diff.py:98:20: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/frame/methods/test_values.py:236:15: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ pandas/tests/frame/methods/test_values.py:237:15: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
... 33 additional changes omitted for project

python-trio/trio (+1 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview

+ src/trio/_tests/type_tests/path.py:17:17: RUF065 Binary operation `/` between a value and itself can be simplified to `1`

pytest-dev/pytest (+2 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview

+ testing/code/test_excinfo.py:427:13: RUF065 Binary operation `//` between a value and itself can be simplified to `1`
+ testing/test_runner.py:549:45: RUF065 Binary operation `/` between a value and itself can be simplified to `1`

astropy/astropy (+27 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview

+ astropy/coordinates/tests/test_representation_arithmetic.py:1475:9: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ astropy/coordinates/tests/test_representation_arithmetic.py:1481:9: RUF065 Binary operation `/` between a value and itself can be simplified to `1`
+ astropy/coordinates/tests/test_representation_arithmetic.py:215:14: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ astropy/coordinates/tests/test_representation_arithmetic.py:247:14: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ astropy/coordinates/tests/test_representation_arithmetic.py:692:19: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ astropy/coordinates/tests/test_representation_arithmetic.py:862:19: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ astropy/io/misc/tests/test_yaml.py:246:10: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ astropy/modeling/tests/test_compound.py:214:9: RUF065 Binary operation `&` between a value and itself can be simplified to `G`
+ astropy/modeling/tests/test_compound.py:235:9: RUF065 Binary operation `-` between a value and itself can be simplified to `0`
+ astropy/modeling/tests/test_compound.py:237:9: RUF065 Binary operation `/` between a value and itself can be simplified to `1`
+ astropy/modeling/tests/test_compound.py:403:9: RUF065 Binary operation `&` between a value and itself can be simplified to `g1`
+ astropy/modeling/tests/test_models.py:821:9: RUF065 Binary operation `&` between a value and itself can be simplified to `models.Polynomial2D(2)`
... 15 additional changes omitted for project

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
RUF065 104 104 0 0 0

@ntBre ntBre self-requested a review July 24, 2025 13:06
@ntBre ntBre added the rule Implementing or modifying a lint rule label Jul 25, 2025
@MichaReiser
Copy link
Member

I haven't looked at the implementation. The rule itself seems reasonable as a Ruff rule because it is about simplification, or even catching potential bugs.

What's less clear to me is if the rule should provide a fix because it's not clear to me whether x & x was intentional or if they mistyped the variable.

@danparizher
Copy link
Contributor Author

danparizher commented Jul 28, 2025

I think we should provide the fixes since these are guaranteed mathematical identities (unlike other simplifications that might change behavior), but add a note in the rule description that developers should verify this was their intent.

We could also consider a separate rule for catching suspicious identity operations without fixes, if that makes more sense.

@ntBre
Copy link
Contributor

ntBre commented Jul 28, 2025

I think Micha's point was more that the user probably meant to write something like x & y rather than just x. The issue isn't that our fix would change behavior but that we could silently preserve the wrong behavior. Instead we should alert the user to the issue so that they can decide what it's supposed to say.

An unsafe or display-only fix might be a good compromise here so that users can apply the fix in an editor, for example, but we don't silently perform the fix without alerting them.

I don't think documenting the issue would be enough when the fix is marked as safe.

@danparizher
Copy link
Contributor Author

Got it. One thing I'm still trying to figure out is the cases for | and %. Looking into why violations aren't being reported for those.

MichaReiser and others added 21 commits July 30, 2025 15:08
…NS` file

This does unfortunately add a fair bit of complexity to the flow
diagram.

Ref astral-sh#19620 (comment)
## Summary

I was a bit stuck on some snapshot differences I was seeing in astral-sh#19415,
but @BurntSushi pointed out that `annotate-snippets` already normalizes
tabs on its own, which was very helpful! Instead of applying this change
directly to the other branch, I wanted to try applying it in
`ruff_linter` first. This should very slightly reduce the number of
changes in astral-sh#19415 proper.

It looks like `annotate-snippets` always expands a tab to four spaces,
whereas I think we were aligning to tab stops:

```diff
  6 | spam(ham[1], { eggs: 2})
  7 | #: E201:1:6
- 8 | spam(   ham[1], {eggs: 2})
-   |      ^^^ E201
+ 8 | spam(    ham[1], {eggs: 2})
+   |      ^^^^ E201
```

```diff
61 | #: E203:2:15 E702:2:16
 62 | if x == 4:
-63 |     print(x, y) ; x, y = y, x
-   |                ^ E203
+63 |     print(x, y)    ; x, y = y, x
+   |                ^^^^ E203
```

```diff
 E27.py:15:6: E271 [*] Multiple spaces after keyword
    |
-13 | True        and False
+13 | True        and    False
 14 | #: E271
 15 | a and  b
    |      ^^ E271
```

I don't think this is too bad and has the major benefit of allowing us
to pass the non-tab-expanded range to `annotate-snippets` in astral-sh#19415,
where it's also displayed in the header. Ruff doesn't have this problem
currently because it uses its own concise diagnostic output as the
header for full diagnostics, where the pre-expansion range is used
directly.

## Test Plan

Existing tests with a few snapshot updates
…stral-sh#19434)

## Summary

Fixes astral-sh#19433

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
## Summary

Add support for `async for` loops and async iterables.

part of astral-sh/ty#151

## Ecosystem impact

```diff
- boostedblob/listing.py:445:54: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
```

This is correct. We now find a true positive in the `# type: ignore`'d
code.

All of the other ecosystem hits are of the type

```diff
trio (https://github.com/python-trio/trio)
+ src/trio/_core/_tests/test_guest_mode.py:532:24: error[not-iterable] Object of type `MemorySendChannel[int] | MemoryReceiveChannel[int]` may not be iterable
```

The message is correct, because only `MemoryReceiveChannel` has an
`__aiter__` method, but `MemorySendChannel` does not. What's not correct
is our inferred type here. It should be `MemoryReceiveChannel[int]`, not
the union of the two. This is due to missing unpacking support for tuple
subclasses, which @AlexWaygood is working on. I don't think this should
block merging this PR, because those wrong types are already there,
without this PR.

## Test Plan

New Markdown tests and snapshot tests for diagnostics.
…e it's necessary to solve a `TypeVar` (astral-sh#19635)

## Summary

This PR improves our generics solver such that we are able to solve the
`TypeVar` in this snippet to `int | str` (the union of the elements in
the heterogeneous tuple) by upcasting the heterogeneous tuple to its
pure-homogeneous-tuple supertype:

```py
def f[T](x: tuple[T, ...]) -> T:
    return x[0]

def g(x: tuple[int, str]):
    reveal_type(f(x))
```

## Test Plan

Mdtests. Some TODOs remain in the mdtest regarding solving `TypeVar`s
for mixed tuples, but I think this PR on its own is a significant step
forward for our generics solver when it comes to tuple types.

---------

Co-authored-by: Douglas Creager <dcreager@dcreager.net>
… ending with backslash (astral-sh#19505)

Issue: astral-sh#19498

## Summary


[missing-required-import](https://docs.astral.sh/ruff/rules/missing-required-import/)
inserts the missing import on the line immediately following the last
line of the docstring. However, if the dosctring is immediately followed
by a continuation token (i.e. backslash) then this leads to a syntax
error because Python interprets the docstring and the inserted import to
be on the same line.

The proposed solution in this PR is to check if the first token after a
file docstring is a continuation character, and if so, to advance an
additional line before inserting the missing import.

## Test Plan

Added a unit test, and the following example was verified manually:

Given this simple test Python file:

```python
"Hello, World!"\

print(__doc__)
```

and this ruff linting configuration in the `pyproject.toml` file:

```toml
[tool.ruff.lint]
select = ["I"]

[tool.ruff.lint.isort]
required-imports = ["import sys"]
```

Without the changes in this PR, the ruff linter would try to insert the
missing import in line 2, resulting in a syntax error, and report the
following:

`error: Fix introduced a syntax error. Reverting all changes.`

With the changes in this PR, ruff correctly advances one more line
before adding the missing import, resulting in the following output:

```python
"Hello, World!"\

import sys

print(__doc__)
```

---------

Co-authored-by: Jim Hoekstra <jim.hoekstra@pacmed.nl>
…9645)

## Summary

Resolves astral-sh/ty#862 by not emitting a
diagnostic.

## Test Plan

Add test to show we don't emit the diagnostic
## Summary

These were added in the 0.5.0 release.
## Summary

Pulls in a bunch of salsa micro-optimizations.
Summary
--

Fixes astral-sh#19640. I'm not sure these are the exact fixes we really want, but
I
reproduced the issue in a 32-bit Docker container and tracked down the
causes,
so I figured I'd open a PR.

As I commented on the issue, the `goto_references` test depends on the
iteration
order of the files in an `FxHashSet` in `Indexed`. In this case, we can
just
sort the output in test code.

Similarly, the tuple case depended on the order of overloads inserted in
an
`FxHashMap`. `FxIndexMap` seemed like a convenient drop-in replacement,
but I
don't know if that will have other detrimental effects. I did have to
change the
assertion for the tuple test, but I think it should now be stable across
architectures.

Test Plan
--

Running the tests in the aforementioned Docker container
…stral-sh#19413)

## Summary

Fixes astral-sh#18729 and fixes astral-sh#16802

## Test Plan

Manually verified via CLI that Ruff no longer enters an infinite loop by
running:
```sh
echo 1 | ruff --isolated check - --select I002,UP010 --fix
```
with `required-imports = ["from __future__ import generator_stop"]` set
in the config, confirming “All checks passed!” and no snapshots were
generated.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
…9672)

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Part of astral-sh#18972

This PR makes [meta-class-abc-meta
(FURB180)](https://docs.astral.sh/ruff/rules/meta-class-abc-meta/#meta-class-abc-meta-furb180)'s
example error out-of-the-box.

[Old example](https://play.ruff.rs/6beca1be-45cd-4e5a-aafa-6a0584c10d64)
```py
class C(metaclass=ABCMeta):
    pass
```

[New example](https://play.ruff.rs/bbad34da-bf07-44e6-9f34-53337e8f57d4)
```py
import abc


class C(metaclass=abc.ABCMeta):
    pass
```

The "Use instead" section as also modified similarly.

## Test Plan

<!-- How was it tested? -->

N/A, no functionality/tests affected
BurntSushi and others added 21 commits August 20, 2025 10:27
We'll want to use these when implementing the
"list modules" API.
These tests capture existing behavior.

I added these when I stumbled upon what I thought was an
oddity: we prioritize `foo.pyi` over `foo.py`, but
prioritize `foo/__init__.py` over `foo.pyi`.

(I plan to investigate this more closely in follow-up
work. Particularly, to look at other type checkers. It
seems like we may want to change this to always prioritize
stubs.)
This makes it a little more flexible to call. For example,
we might have a `StmtImport` and not a `StmtImportFrom`.
Previously, if the module was just `foo-stubs`, we'd skip over
stripping the `-stubs` suffix which would lead to us returning
`None`.

This function is now a little convoluted and could be simpler
if we did an intermediate allocation. But I kept the iterative
approach and added a special case to handle `foo-stubs`.
This will make it easier to emit this info into snapshots for
testing.
The actual implementation wasn't too bad. It's not long
but pretty fiddly. I copied over the tests from the existing
module resolver and adapted them to work with this API. Then
I added a number of my own tests as well.
These tests were added as a regression check that a panic
didn't occur. So we were asserting a bit more than necessary.
In particular, these will soon return completions for modules,
which creates large snapshots that we don't need.

So modify these to just check there is sensible output that
doesn't panic.
This makes `import <CURSOR>` and `from <CURSOR>` completions work.

This also makes `import os.<CURSOR>` and `from os.<CURSOR>`
completions work. In this case, we are careful to only offer
submodule completions.
This ensures there is some level of consistency between the APIs.

This did require exposing a couple more things on `Module` for good
error messages. This also motivated a switch to an interned struct
instead of a tracked struct. This ensures that `list_modules` and
`resolve_modules` reuse the same `Module` values when the inputs are the
same.

Ref astral-sh#19883 (comment)
This basically splits `list_modules` into a higher level "aggregation"
routine and a lower level "get modules for one search path" routine.
This permits Salsa to cache the lower level components, e.g., many
search paths refer to directories that rarely change. This saves us
interaction with the system.

This did require a fair bit of surgery in terms of being careful about
adding file roots. Namely, now that we rely even more on file roots
existing for correct handling of cache invalidation, there were several
spots in our code that needed to be updated to add roots (that we
weren't previously doing). This feels Not Great, and it would be better
if we had some kind of abstraction that handled this for us. But it
isn't clear to me at this time what that looks like.
Summary
--

I noticed while working on astral-sh#20006 that we had a custom `unwrap` function
for `Option`. This has been const on stable since 1.83
([docs](https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap),
[release notes](https://blog.rust-lang.org/2024/11/28/Rust-1.83.0/)), so
I think it's safe to use now. I grepped a bit for related todos and
found this one for `AsciiCharSet` but no others.

Test Plan
--

Existing tests
…20002)

This commit corrects the type checker's behavior when handling
`dataclass_transform` decorators that don't explicitly specify
`field_specifiers`. According to [PEP 681 (Data Class
Transforms)](https://peps.python.org/pep-0681/#dataclass-transform-parameters),
when `field_specifiers` is not provided, it defaults to an empty tuple,
meaning no field specifiers are supported and
`dataclasses.field`/`dataclasses.Field` calls should be ignored.

Fixes astral-sh/ty#980
…#19514)

## Summary

Part of astral-sh#2331

## Test Plan

<!-- How was it tested? -->
`cargo nextest run flake8_use_pathlib`
…en they are used (`UP010`) (astral-sh#19769)

## Summary
Resolves astral-sh#19561

Fixes the [unnecessary-future-import
(UP010)](https://docs.astral.sh/ruff/rules/unnecessary-future-import/)
rule to correctly identify when imported __future__ modules are actually
used in the code, preventing false positives.

I assume there is no way to check usage in `analyze::statements`,
because we don't have any usage bindings for imports. To determine
unused imports, we have to fully scan the file to create bindings and
then check usage, similar to [unused-import
(F401)](https://docs.astral.sh/ruff/rules/unused-import/#unused-import-f401).
So, `Rule::UnnecessaryFutureImport` was moved from the
`analyze::statements` to the `analyze::deferred_scopes` stage. This
caused the need to change the logic of future import handling to a
bindings-based approach.

Also, the diagnostic report was changed.
Before
```
  |
1 | from __future__ import nested_scopes, generators
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP010
```
after
```
  |
1 | from __future__ import nested_scopes, generators
  |                        ^^^^^^^^^^^^^ UP010
```

I believe this is the correct way, because `generators` may be used, but
`nested_scopes` is not.

### Special case
I've found out about some specific case.
```python
from __future__ import nested_scopes

nested_scopes = 1
```
Here we can treat `nested_scopes` as an unused import because the
variable `nested_scopes` shadows it and we can safely remove the future
import (my fix does it).

But
[F401](https://docs.astral.sh/ruff/rules/unused-import/#unused-import-f401)
not triggered for such case
([sandbox](https://play.ruff.rs/296d9c7e-0f02-4659-b0c0-78cc21f3de76))
```
from foo import print_function

print_function = 1
```
In my mind, `print_function` here is an unused import and should be
deleted (my IDE highlight it). What do you think?

## Test Plan

Added test cases and snapshots:
- Split test file into separate _0 and _1 files for appropriate checks.
- Added test cases to verify fixes when future module are used.

---------

Co-authored-by: Igor Drokin <drokinii1017@gmail.com>
Copy link
Contributor

github-actions bot commented Aug 20, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

Copy link
Contributor

mypy_primer results

No ecosystem changes detected ✅
No memory usage changes detected ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.