Skip to content

[pylint] Flag chained associative operators + Explicitly account for unary ops (non-augmented-assignment, PLR6104) - #27188

Open
Avasam wants to merge 2 commits into
astral-sh:mainfrom
Avasam:non-augmented-assignment-(PLR6104)-chained-associative-operators
Open

[pylint] Flag chained associative operators + Explicitly account for unary ops (non-augmented-assignment, PLR6104)#27188
Avasam wants to merge 2 commits into
astral-sh:mainfrom
Avasam:non-augmented-assignment-(PLR6104)-chained-associative-operators

Conversation

@Avasam

@Avasam Avasam commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

I kept addition and multiplication chains for floating point numbers despite them not being associative due to floating point precision ((0.1 + 0.2) + 0.3 != 0.1 + (0.2 + 0.3)). The fix is already marked as unsafe and this isn't any more unsafe than it already is for mutable types. (see #12890 ). But opinions may differ.

Open question

  1. Should I migrate the snapshot to the newer mdtest format ?

  2. Claude did flag the following:

    non_augmented_assignment.rs: 🔵 nit: contains_line_break doesn't distinguish breaks inside string literals, so s = s + """a\nb""" + "c" gets needless parens (s += ("""a\nb""" + "c")). Low value to fix; note it if you care about fix noise.
    non_augmented_assignment.rs: 🔵 nit: an already-parenthesized first operand yields doubled parens — x = x + (\n1\n) + 2x += ((\n1\n) + 2). Cosmetic only.

    If you think it's worth fixing / accounting for.

Test Plan

  1. By looking at the new snapshot results.
  2. Looking at ecosystem results
  3. Ran cargo run -p ruff -- check --no-cache --select=PLR6104 --preview on a few of the projects I maintain.

Coding Agent disclaimer

Code was fully written by Claude Opus 5 with a few passes of caveman-review. I did a glancing review of code changes given my limited Rust knowledge. PR description fully handwritten. Docs and tests human reviewed.

Only two-operand assignments were matched, so `x = x * 2 * y` and
`x = 2 * 3 * x` were missed. Python parses `a * b * c` as
`(a * b) * c`, so the target is found by walking the chain's left
spine.

Chains are only rewritten for operators that are both commutative and
associative. `@` is associative but excluded: `x @= a @ b` builds the
full matrix-matrix product first, which can cost orders of magnitude
more than the pair of matrix-vector products it replaces.

A multi-line operand may only have been valid because the whole
assigned value was parenthesized, and those parentheses are dropped
with the rest of the statement, so the fix adds its own. This also
corrects existing broken output for implicitly concatenated strings.

Documents three fix hazards that were previously unstated: comment
loss, float regrouping, and NumPy in-place operators rejecting shape
or dtype changes that the plain form allows.

Closes astral-sh#11656
Relates to astral-sh#27051
Expand rule safety recommendations for astral-sh#12890

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@astral-sh-bot
astral-sh-bot Bot requested a review from ntBre July 26, 2026 02:12
@astral-sh-bot

astral-sh-bot Bot commented Jul 26, 2026

Copy link
Copy Markdown

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+37 -0 violations, +0 -0 fixes in 12 projects; 45 projects unchanged)

PlasmaPy/PlasmaPy (+2 -0 violations, +0 -0 fixes)

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

+ src/plasmapy/plasma/grids.py:1171:13: non-augmented-assignment Use `*=` to perform an augmented assignment directly
+ tools/export_ionization_energy.py:230:9: non-augmented-assignment Use `+=` to perform an augmented assignment directly

apache/airflow (+9 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL

+ airflow-core/src/airflow/dag_processing/importers/python_importer.py:59:5: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ dev/breeze/src/airflow_breeze/utils/run_utils.py:369:9: non-augmented-assignment Use `|=` to perform an augmented assignment directly
+ providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py:2015:21: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ providers/teradata/src/airflow/providers/teradata/operators/teradata_compute_cluster.py:252:13: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ providers/teradata/src/airflow/providers/teradata/operators/teradata_compute_cluster.py:254:13: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ providers/teradata/src/airflow/providers/teradata/operators/teradata_compute_cluster.py:256:13: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ providers/teradata/src/airflow/providers/teradata/operators/teradata_compute_cluster.py:258:13: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ providers/teradata/src/airflow/providers/teradata/operators/teradata_compute_cluster.py:287:21: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ providers/teradata/src/airflow/providers/teradata/operators/teradata_compute_cluster.py:363:13: non-augmented-assignment Use `+=` to perform an augmented assignment directly

apache/superset (+2 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL

+ superset/advanced_data_type/plugins/internet_address.py:97:17: non-augmented-assignment Use `&=` to perform an augmented assignment directly
+ superset/utils/date_parser.py:500:9: non-augmented-assignment Use `+=` to perform an augmented assignment directly

aws/aws-sam-cli (+2 -0 violations, +0 -0 fixes)

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

+ samcli/lib/package/local_files_utils.py:62:9: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ samcli/lib/schemas/schemas_directory_hierarchy_builder.py:16:9: non-augmented-assignment Use `+=` to perform an augmented assignment directly

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

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

+ ibis/backends/risingwave/__init__.py:45:9: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ ibis/backends/risingwave/__init__.py:47:9: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ ibis/backends/risingwave/__init__.py:49:13: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ ibis/backends/risingwave/__init__.py:872:9: non-augmented-assignment Use `+=` to perform an augmented assignment directly

milvus-io/pymilvus (+1 -0 violations, +0 -0 fixes)

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

+ pymilvus/bulk_writer/bulk_writer.py:399:13: non-augmented-assignment Use `+=` to perform an augmented assignment directly

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

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

+ pandas/tests/strings/test_api.py:204:5: non-augmented-assignment Use `+=` to perform an augmented assignment directly

rotki/rotki (+1 -0 violations, +0 -0 fixes)

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

+ rotkehlchen/exchanges/okx.py:393:17: non-augmented-assignment Use `*=` to perform an augmented assignment directly

zulip/zulip (+3 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL

+ corporate/views/installation_activity.py:277:13: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ zerver/lib/generate_test_data.py:174:5: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ zerver/lib/generate_test_data.py:182:5: non-augmented-assignment Use `+=` to perform an augmented assignment directly

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

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

+ src/_pytest/_py/path.py:743:13: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ src/_pytest/monkeypatch.py:325:13: non-augmented-assignment Use `+=` to perform an augmented assignment directly

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

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

+ astropy/modeling/polynomial.py:383:9: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ astropy/utils/introspection.py:53:9: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ astropy/visualization/wcsaxes/grid_paths.py:53:5: non-augmented-assignment Use `|=` to perform an augmented assignment directly

home-assistant/core (+7 -0 violations, +0 -0 fixes)

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

+ homeassistant/components/bluesound/media_player.py:385:13: non-augmented-assignment Use `|=` to perform an augmented assignment directly
+ homeassistant/components/bluesound/media_player.py:399:13: non-augmented-assignment Use `|=` to perform an augmented assignment directly
+ homeassistant/components/ecobee/climate.py:275:13: non-augmented-assignment Use `|=` to perform an augmented assignment directly
+ homeassistant/components/homekit/type_cameras.py:391:13: non-augmented-assignment Use `+=` to perform an augmented assignment directly
+ homeassistant/components/osramlightify/light.py:225:13: non-augmented-assignment Use `|=` to perform an augmented assignment directly
+ homeassistant/components/webostv/media_player.py:215:17: non-augmented-assignment Use `|=` to perform an augmented assignment directly
+ tests/components/webostv/test_media_player.py:602:5: non-augmented-assignment Use `|=` to perform an augmented assignment directly

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
non-augmented-assignment 37 37 0 0 0

@ntBre

ntBre commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Thanks for looking into this, but I think it may be another needs-decision case. To me, expanding the rule to these more niche cases isn't really worth the complications in the rule implementation and explanation, but I'm open to other perspectives.

I also wonder if this could make the problem tracked in #12890 even more common. That seems to be the case from a quick look at the new pandas ecosystem hit.

@ntBre ntBre added rule Implementing or modifying a lint rule needs-decision Awaiting a decision from a maintainer preview Related to preview mode features labels Jul 27, 2026
@Avasam

Avasam commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

The PR did reveal additional complexities I didn't initially think of before trying to implement. Even if it doesn't get merged, it did have exploratory value.

Maybe it's worth just waiting for types to be available to Ruff? To properly separate safe vs unsafe fixes (and reducing false positives) before expanding the rule.

Also open to more opinions perspectives. (I would take the changes I did, but that's just me and my usages)

Comment thread crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs Outdated
/// [`ast::UnaryOp::Not`] is left out because it can only appear here parenthesized, as in
/// `x = (not 0) + x`; `not 0 + x` parses as `not (0 + x)` instead. The extra case isn't worth
/// supporting for a form nobody writes.
fn is_numeric_constant(expr: &Expr) -> bool {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted to #27251 (and simplified since that doesn't care about chained operators yet)
Which would reduce the amount of new tests here.

@Avasam Avasam changed the title [pylint] Flag chained associative operators + Explicitly account for unary ops (PLR6104) [pylint] Flag chained associative operators + Explicitly account for unary ops (non-augmented-assignment, PLR6104) Jul 28, 2026
ntBre pushed a commit that referenced this pull request Jul 28, 2026
…nt` (`PLR6104`) (#27250)

## Summary
Add missing fix safety gotchas for [non-augmented-assignment
(PLR6104)](https://docs.astral.sh/ruff/rules/non-augmented-assignment/#non-augmented-assignment-plr6104)

Extracted from #27188

## Test Plan

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

Labels

needs-decision Awaiting a decision from a maintainer preview Related to preview mode features rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PLR6104 misses cases with chained associative operators

2 participants