Skip to content

Comments

[ruff] Add fix for none-not-at-end-of-union (RUF036) #22829

Open
anishgirianish wants to merge 6 commits intoastral-sh:mainfrom
anishgirianish:feat/ruf036-autofix
Open

[ruff] Add fix for none-not-at-end-of-union (RUF036) #22829
anishgirianish wants to merge 6 commits intoastral-sh:mainfrom
anishgirianish:feat/ruf036-autofix

Conversation

@anishgirianish
Copy link
Contributor

Summary

Adds an autofix for RUF036 that moves None to the end of union type annotations.

Closes #15136

The fix:

  • Preserves the union style (PEP 604 | vs typing.Union)
  • Preserves duplicate None values instead of deduplicating
  • Skips nested unions to avoid flattening (e.g., None | Union[int, str])
  • Marked unsafe when comments are present in the annotation

Test Plan

cargo nextest run -p ruff_linter - added test cases for nested unions, comments,
default arguments, and mixed styles.

@anishgirianish anishgirianish changed the title [ruff] Add fix for none-not-at-end-of-union (RUF036) [ruff] Add fix for none-not-at-end-of-union (RUF036) Jan 24, 2026
@amyreese amyreese requested a review from ntBre February 13, 2026 23:07
@anishgirianish
Copy link
Contributor Author

@amyreese Thanks for the review! I've addressed all the feedback:

  • Changed "type annotation" to "type union" in the message and fix title
  • Refactored generate_pep604_fix and generate_typing_union_fix to return Edit, building the Fix in generate_fix
  • Removed .copied() by simplifying to [other_exprs, none_exprs].concat()

Ready for another look whenever you have a chance. Thank you!

@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 14, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+367 -367 violations, +0 -0 fixes in 18 projects; 38 projects unchanged)

DisnakeDev/disnake (+10 -10 violations, +0 -0 fixes)

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

+ disnake/gateway.py:287:37: RUF036 [*] `None` not at the end of the type union.
- disnake/gateway.py:287:43: RUF036 `None` not at the end of the type annotation.
+ disnake/i18n.py:103:15: RUF036 [*] `None` not at the end of the type union.
- disnake/i18n.py:103:35: RUF036 `None` not at the end of the type annotation.
+ disnake/i18n.py:113:15: RUF036 [*] `None` not at the end of the type union.
- disnake/i18n.py:113:35: RUF036 `None` not at the end of the type annotation.
+ disnake/i18n.py:137:44: RUF036 [*] `None` not at the end of the type union.
- disnake/i18n.py:137:50: RUF036 `None` not at the end of the type annotation.
+ disnake/i18n.py:30:37: RUF036 [*] `None` not at the end of the type union.
- disnake/i18n.py:30:43: RUF036 `None` not at the end of the type annotation.
... 10 additional changes omitted for project

RasaHQ/rasa (+3 -3 violations, +0 -0 fixes)

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

+ rasa/shared/core/events.py:597:42: RUF036 [*] `None` not at the end of the type union.
- rasa/shared/core/events.py:597:48: RUF036 `None` not at the end of the type annotation.
+ rasa/shared/core/events.py:624:25: RUF036 [*] `None` not at the end of the type union.
- rasa/shared/core/events.py:624:31: RUF036 `None` not at the end of the type annotation.
+ rasa/utils/tensorflow/models.py:247:29: RUF036 [*] `None` not at the end of the type union.
- rasa/utils/tensorflow/models.py:247:35: RUF036 `None` not at the end of the type annotation.

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

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

+ airflow-core/src/airflow/api_fastapi/common/parameters.py:1086:22: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/api_fastapi/common/parameters.py:1086:22: RUF036 `None` not at the end of the type annotation.
+ airflow-core/src/airflow/api_fastapi/common/parameters.py:203:20: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/api_fastapi/common/parameters.py:203:20: RUF036 `None` not at the end of the type annotation.
+ airflow-core/src/airflow/api_fastapi/common/parameters.py:206:22: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/api_fastapi/common/parameters.py:206:22: RUF036 `None` not at the end of the type annotation.
+ airflow-core/src/airflow/api_fastapi/common/parameters.py:210:26: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/api_fastapi/common/parameters.py:210:26: RUF036 `None` not at the end of the type annotation.
+ airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py:38:65: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py:38:65: RUF036 `None` not at the end of the type annotation.
+ airflow-core/src/airflow/example_dags/plugins/event_listener.py:102:12: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/example_dags/plugins/event_listener.py:102:12: RUF036 `None` not at the end of the type annotation.
+ airflow-core/src/airflow/executors/base_executor.py:155:13: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/executors/base_executor.py:155:13: RUF036 `None` not at the end of the type annotation.
... 196 additional changes omitted for project

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

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

+ superset/db_engine_specs/gsheets.py:310:26: RUF036 [*] `None` not at the end of the type union.
- superset/db_engine_specs/gsheets.py:310:26: RUF036 `None` not at the end of the type annotation.
+ superset/jinja_context.py:101:10: RUF036 [*] `None` not at the end of the type union.
- superset/jinja_context.py:101:16: RUF036 `None` not at the end of the type annotation.

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

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

+ src/bokeh/client/websocket.py:73:85: RUF036 [*] `None` not at the end of the type union.
- src/bokeh/client/websocket.py:73:85: RUF036 `None` not at the end of the type annotation.
+ src/bokeh/embed/standalone.py:84:24: RUF036 [*] `None` not at the end of the type union.
- src/bokeh/embed/standalone.py:84:30: RUF036 `None` not at the end of the type annotation.
+ src/bokeh/util/tornado.py:231:26: RUF036 [*] `None` not at the end of the type union.
- src/bokeh/util/tornado.py:231:26: RUF036 `None` not at the end of the type annotation.

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

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

+ ibis/backends/__init__.py:1294:54: RUF036 [*] `None` not at the end of the type union.
- ibis/backends/__init__.py:1294:54: RUF036 `None` not at the end of the type annotation.
+ ibis/backends/sql/__init__.py:725:43: RUF036 [*] `None` not at the end of the type union.
- ibis/backends/sql/__init__.py:725:43: RUF036 `None` not at the end of the type annotation.

langchain-ai/langchain (+10 -10 violations, +0 -0 fixes)

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

+ libs/langchain/langchain_classic/evaluation/parsing/base.py:152:10: RUF036 [*] `None` not at the end of the type union.
- libs/langchain/langchain_classic/evaluation/parsing/base.py:152:24: RUF036 `None` not at the end of the type annotation.
+ libs/langchain/langchain_classic/evaluation/parsing/json_distance.py:94:41: RUF036 [*] `None` not at the end of the type union.
- libs/langchain/langchain_classic/evaluation/parsing/json_distance.py:94:55: RUF036 `None` not at the end of the type annotation.
+ libs/langchain/langchain_classic/evaluation/parsing/json_schema.py:67:41: RUF036 [*] `None` not at the end of the type union.
- libs/langchain/langchain_classic/evaluation/parsing/json_schema.py:67:55: RUF036 `None` not at the end of the type annotation.
+ libs/langchain/langchain_classic/indexes/_sql_record_manager.py:93:17: RUF036 [*] `None` not at the end of the type union.
- libs/langchain/langchain_classic/indexes/_sql_record_manager.py:93:17: RUF036 `None` not at the end of the type annotation.
+ libs/langchain_v1/langchain/agents/middleware/types.py:806:10: RUF036 [*] `None` not at the end of the type union.
- libs/langchain_v1/langchain/agents/middleware/types.py:806:42: RUF036 `None` not at the end of the type annotation.
... 10 additional changes omitted for project

latchbio/latch (+5 -5 violations, +0 -0 fixes)

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

+ src/latch/registry/types.py:38:43: RUF036 [*] `None` not at the end of the type union.
- src/latch/registry/types.py:47:5: RUF036 `None` not at the end of the type annotation.
+ src/latch_cli/services/get.py:38:26: RUF036 [*] `None` not at the end of the type union.
- src/latch_cli/services/get.py:38:32: RUF036 `None` not at the end of the type annotation.
+ src/latch_cli/snakemake/config/utils.py:200:50: RUF036 [*] `None` not at the end of the type union.
- src/latch_cli/snakemake/config/utils.py:200:56: RUF036 `None` not at the end of the type annotation.
+ src/latch_cli/snakemake/config/utils.py:25:24: RUF036 [*] `None` not at the end of the type union.
- src/latch_cli/snakemake/config/utils.py:25:53: RUF036 `None` not at the end of the type annotation.
+ src/latch_sdk_gql/utils.py:37:28: RUF036 [*] `None` not at the end of the type union.
- src/latch_sdk_gql/utils.py:38:59: RUF036 `None` not at the end of the type annotation.

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

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

+ pymilvus/client/search_result.py:888:51: RUF036 [*] `None` not at the end of the type union.
- pymilvus/client/search_result.py:888:57: RUF036 `None` not at the end of the type annotation.

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

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

+ pandas/_libs/json.pyi:14:22: RUF036 [*] `None` not at the end of the type union.
- pandas/_libs/json.pyi:14:22: RUF036 `None` not at the end of the type annotation.
+ pandas/_libs/tslibs/timedeltas.pyi:147:36: RUF036 [*] `None` not at the end of the type union.
- pandas/_libs/tslibs/timedeltas.pyi:147:36: RUF036 `None` not at the end of the type annotation.
+ pandas/_libs/tslibs/timestamps.pyi:31:25: RUF036 [*] `None` not at the end of the type union.
- pandas/_libs/tslibs/timestamps.pyi:31:41: RUF036 `None` not at the end of the type annotation.
+ pandas/core/arrays/datetimelike.py:1075:45: RUF036 [*] `None` not at the end of the type union.
- pandas/core/arrays/datetimelike.py:1075:45: RUF036 `None` not at the end of the type annotation.
+ pandas/core/dtypes/cast.py:186:29: RUF036 [*] `None` not at the end of the type union.
- pandas/core/dtypes/cast.py:186:38: RUF036 `None` not at the end of the type annotation.
... 46 additional changes omitted for project

python/typeshed (+143 -143 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select E,F,FA,I,PYI,RUF,UP,W

+ stdlib/_interpreters.pyi:30:6: RUF036 [*] `None` not at the end of the type union.
- stdlib/_interpreters.pyi:30:6: RUF036 `None` not at the end of the type annotation.
+ stdlib/_ssl.pyi:82:58: RUF036 [*] `None` not at the end of the type union.
- stdlib/_ssl.pyi:82:58: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1096:51: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1096:96: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1099:51: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1099:96: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1754:26: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1754:26: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1764:26: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1764:26: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1774:26: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1774:26: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1784:26: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1784:26: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1794:26: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1794:26: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1803:26: RUF036 [*] `None` not at the end of the type union.
... 267 additional changes omitted for project

... Truncated remaining completed project reports due to GitHub comment length restrictions

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
RUF036 734 367 367 0 0

@ntBre
Copy link
Contributor

ntBre commented Feb 23, 2026

Thanks @anishgirianish for working on this! I haven't looked at the diff yet, so apologies if you've addressed all of these, but I was hoping you could check whether or not you had resolved my comment on a previous version of this PR? See #18964 (review) and the comments it links to on an even earlier version. I think that's a good summary of the missing pieces from the earlier iterations and would help me get started reviewing this PR.

@anishgirianish
Copy link
Contributor Author

Thanks @anishgirianish for working on this! I haven't looked at the diff yet, so apologies if you've addressed all of these, but I was hoping you could check whether or not you had resolved my comment on a previous version of this PR? See #18964 (review) and the comments it links to on an even earlier version. I think that's a good summary of the missing pieces from the earlier iterations and would help me get started reviewing this PR.

Sure will do thanks

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Autofix for none-not-at-end-of-union (RUF036)

3 participants