Compare commits

...

56 Commits

Author SHA1 Message Date
David Peter
e935bc5578 [ty] enum.Flag 2025-07-29 16:52:42 +02:00
Alex Waygood
81867ea7ce [ty] Discard Definitions when normalizing Signatures (#19615) 2025-07-29 14:37:47 +01:00
Brent Westbrook
a54061e757 [ty] Fix empty spans following a line terminator and unprintable character spans in diagnostics (#19535)
## Summary

This was previously the last commit in #19415, split out to make it
easier to review. This applies the fixes from c9b99e4, 5021f32, and
2922490cb8 to the new rendering code in `ruff_db`. I initially intended
only to fix the empty span after a line terminator (as you can see in
the branch name), but the two fixes were tied pretty closely together,
and my initial fix for the empty spans needed a big change after trying
to handle unprintable characters too. I can still split this up if it
would help with review. I would just start with the unprintable
characters first.

The implementation here is essentially copy-pasted from
`ruff_linter::message::text.rs`, with the `SourceCode` struct renamed to
`EscapedSourceCode` since there's already a `SourceCode` in scope in
`render.rs`. It's also updated slightly to account for the multiple
annotations for a single snippet. The original implementation used some
types from the `line_width` module from `ruff_linter`. I copied over
heavily stripped-down versions of these instead of trying to import
them. We could inline the remaining code entirely, if we want, but I
thought it was nice enough to keep.

I also moved over `ceil_char_boundary`, which is unchanged except to
make it a free function taking a `&str` instead of a `Locator` method.
All of this code could be deleted from `ruff_linter` if we also move
over the `grouped` output format, which will be the last user after
#19415.

## Test Plan

I added new tests in `ruff_linter` that call into the new rendering code
to snapshot the diagnostics for the affected cases. These are copies of
existing snapshots in Ruff, so it's helpful to compare them. These are a
bit noisy because of the other rendering differences in the header, but
all of the `^^^` indicators should be the same.

<details><summary>`empty_span_after_line_terminator` diff</summary>

```diff
diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__text__tests__empty_span_after_line_terminator.snap
index 5ade4346e0..6df75c16f0 100644
--- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap
+++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__text__tests__empty_span_after_line_terminator.snap
@@ -1,17 +1,20 @@
 ---
-source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
+source: crates/ruff_linter/src/message/text.rs
+expression: value.to_string()
 ---
-E11.py:9:1: E112 Expected an indented block
+error[no-indented-block]: Expected an indented block
+  --> E11.py:9:1
    |
  7 | #: E112
  8 | if False:
  9 | print()
-   | ^ E112
+   | ^
 10 | #: E113
 11 | print()
    |
 
-E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
+error[invalid-syntax]: SyntaxError: Expected an indented block after `if` statement
+  --> E11.py:9:1
    |
  7 | #: E112
  8 | if False:
@@ -21,7 +24,8 @@ E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
 11 | print()
    |
 
-E11.py:12:1: SyntaxError: Unexpected indentation
+error[invalid-syntax]: SyntaxError: Unexpected indentation
+  --> E11.py:12:1
    |
 10 | #: E113
 11 | print()
@@ -31,7 +35,8 @@ E11.py:12:1: SyntaxError: Unexpected indentation
 14 | mimetype = 'application/x-directory'
    |
 
-E11.py:14:1: SyntaxError: Expected a statement
+error[invalid-syntax]: SyntaxError: Expected a statement
+  --> E11.py:14:1
    |
 12 |     print()
 13 | #: E114 E116
@@ -41,17 +46,19 @@ E11.py:14:1: SyntaxError: Expected a statement
 16 | create_date = False
    |
 
-E11.py:45:1: E112 Expected an indented block
+error[no-indented-block]: Expected an indented block
+  --> E11.py:45:1
    |
 43 | #: E112
 44 | if False:  #
 45 | print()
-   | ^ E112
+   | ^
 46 | #:
 47 | if False:
    |
 
-E11.py:45:1: SyntaxError: Expected an indented block after `if` statement
+error[invalid-syntax]: SyntaxError: Expected an indented block after `if` statement
+  --> E11.py:45:1
    |
 43 | #: E112
 44 | if False:  #
```

</details>

<details><summary>`unprintable_characters` diff</summary>

```diff
diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__text__tests__unprintable_characters.snap
index 52cfdf9cce..fcfa1ac9f1 100644
--- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap
+++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__text__tests__unprintable_characters.snap
@@ -1,161 +1,115 @@
 ---
-source: crates/ruff_linter/src/rules/pylint/mod.rs
+source: crates/ruff_linter/src/message/text.rs
+expression: value.to_string()
 ---
-invalid_characters.py:24:12: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:24:12
    |
 22 | cr_ok = f'\\r'
 23 |
 24 | sub = 'sub '
-   |            ^ PLE2512
+   |            ^
 25 | sub = f'sub '
    |
-   = help: Replace with escape sequence
+help: Replace with escape sequence
 
-ℹ Safe fix
-21 21 | cr_ok = '\\r'
-22 22 | cr_ok = f'\\r'
-23 23 | 
-24    |-sub = 'sub '
-   24 |+sub = 'sub \x1A'
-25 25 | sub = f'sub '
-26 26 | 
-27 27 | sub_ok = '\x1a'
-
-invalid_characters.py:25:13: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:25:13
    |
 24 | sub = 'sub '
 25 | sub = f'sub '
-   |             ^ PLE2512
+   |             ^
 26 |
 27 | sub_ok = '\x1a'
    |
-   = help: Replace with escape sequence
-
-ℹ Safe fix
-22 22 | cr_ok = f'\\r'
-23 23 | 
-24 24 | sub = 'sub '
-25    |-sub = f'sub '
-   25 |+sub = f'sub \x1A'
-26 26 | 
-27 27 | sub_ok = '\x1a'
-28 28 | sub_ok = f'\x1a'
+help: Replace with escape sequence
 
-invalid_characters.py:55:25: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:55:25
    |
 53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ ​​"
 54 |
 55 | nested_fstrings = f'␈{f'{f'␛'}'}'
-   |                         ^ PLE2512
+   |                         ^
 56 |
 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
    |
-   = help: Replace with escape sequence
-
-ℹ Safe fix
-52 52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ ​​"
-53 53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ ​​"
-54 54 | 
-55    |-nested_fstrings = f'␈{f'{f'␛'}'}'
-   55 |+nested_fstrings = f'␈{f'\x1A{f'␛'}'}'
-56 56 | 
-57 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
-58 58 | x = f"""}}ab"""
+help: Replace with escape sequence
 
-invalid_characters.py:58:12: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:58:12
    |
 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
 58 | x = f"""}}ab"""
-   |            ^ PLE2512
+   |            ^
 59 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998256
 60 | x = f"""}}a␛b"""
    |
-   = help: Replace with escape sequence
+help: Replace with escape sequence
 
-ℹ Safe fix
-55 55 | nested_fstrings = f'␈{f'{f'␛'}'}'
-56 56 | 
-57 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
-58    |-x = f"""}}ab"""
-   58 |+x = f"""}}a\x1Ab"""
-59 59 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998256
-60 60 | x = f"""}}a␛b"""
-61 61 | 
-
-invalid_characters.py:64:12: PLE2512 Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:64:12
    |
 63 | # https://github.com/astral-sh/ruff/issues/13294
 64 | print(r"""␈␛�​
-   |            ^ PLE2512
+   |            ^
 65 | """)
 66 | print(fr"""␈␛�​
    |
-   = help: Replace with escape sequence
+help: Replace with escape sequence
 
-invalid_characters.py:66:13: PLE2512 Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:66:13
    |
 64 | print(r"""␈␛�​
 65 | """)
 66 | print(fr"""␈␛�​
-   |             ^ PLE2512
+   |             ^
 67 | """)
 68 | print(Rf"""␈␛�​
    |
-   = help: Replace with escape sequence
+help: Replace with escape sequence
 
-invalid_characters.py:68:13: PLE2512 Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:68:13
    |
 66 | print(fr"""␈␛�​
 67 | """)
 68 | print(Rf"""␈␛�​
-   |             ^ PLE2512
+   |             ^
 69 | """)
    |
-   = help: Replace with escape sequence
+help: Replace with escape sequence
 
-invalid_characters.py:73:9: PLE2512 Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:73:9
    |
 71 | # https://github.com/astral-sh/ruff/issues/18815
 72 | b = "\␈"
 73 | sub = "\"
-   |         ^ PLE2512
+   |         ^
 74 | esc = "\␛"
 75 | zwsp = "\​"
    |
-   = help: Replace with escape sequence
+help: Replace with escape sequence
 
-invalid_characters.py:80:25: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:80:25
    |
 78 | # tstrings
 79 | esc = t'esc esc ␛'
 80 | nested_tstrings = t'␈{t'{t'␛'}'}'
-   |                         ^ PLE2512
+   |                         ^
 81 | nested_ftstrings = t'␈{f'{t'␛'}'}'
    |
-   = help: Replace with escape sequence
-
-ℹ Safe fix
-77 77 | 
-78 78 | # tstrings
-79 79 | esc = t'esc esc ␛'
-80    |-nested_tstrings = t'␈{t'{t'␛'}'}'
-   80 |+nested_tstrings = t'␈{t'\x1A{t'␛'}'}'
-81 81 | nested_ftstrings = t'␈{f'{t'␛'}'}'
-82 82 | 
+help: Replace with escape sequence
 
-invalid_characters.py:81:26: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:81:26
    |
 79 | esc = t'esc esc ␛'
 80 | nested_tstrings = t'␈{t'{t'␛'}'}'
 81 | nested_ftstrings = t'␈{f'{t'␛'}'}'
-   |                          ^ PLE2512
+   |                          ^
    |
-   = help: Replace with escape sequence
-
-ℹ Safe fix
-78 78 | # tstrings
-79 79 | esc = t'esc esc ␛'
-80 80 | nested_tstrings = t'␈{t'{t'␛'}'}'
-81    |-nested_ftstrings = t'␈{f'{t'␛'}'}'
-   81 |+nested_ftstrings = t'␈{f'\x1A{t'␛'}'}'
-82 82 |
+help: Replace with escape sequence
```

</details>
2025-07-29 08:25:58 -04:00
Brent Westbrook
19569bf838 Add LinterContext::settings to avoid passing separate settings (#19608)
Summary
--

I noticed while reviewing #19390 that in `check_tokens` we were still
passing
around an extra `LinterSettings`, despite all of the same functions also
receiving a `LintContext` with its own settings.

This PR adds the `LintContext::settings` method and calls that instead
of using
the separate `LinterSettings`.

Test Plan
--

Existing tests
2025-07-29 08:13:22 -04:00
Charlie Marsh
e0f4f25d28 Support .pyi files in ruff analyze graph (#19611)
## Summary

We now return both the `.pyi` and `.py` files. Previously, we only
returned the `.pyi` file.
2025-07-28 22:00:27 -04:00
github-actions[bot]
c6a123290d [ty] Sync vendored typeshed stubs (#19607)
Co-authored-by: typeshedbot <>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-07-28 22:06:33 +00:00
Alex Waygood
d4f64cd474 [ty] Bump docstring-adder pin (#19606) 2025-07-28 22:59:56 +01:00
Igor Drokin
e4f64480da [perflint] Ignore rule if target is global or nonlocal (PERF401) (#19539)
## Summary

Resolves #19531

I've implemented a check to determine whether the for_stmt target is
declared as global or nonlocal. I believe we should skip the rule in all
such cases, since variables declared this way are intended for use
outside the loop scope, making value changes expected behavior.

## Test Plan

Added two test cases for global and nonlocal variable to snapshot.
2025-07-28 17:03:22 -04:00
Micha Reiser
4016aff057 Add license classifier back to pyproject.toml (#19599)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-07-28 20:58:16 +01:00
UnboundVariable
24134837f3 [ty] Add stub mapping support to signature help (#19570)
This PR improves the "signature help" language server feature in two
ways:
1. It adds support for the recently-introduced "stub mapper" which maps
symbol declarations within stubs to their implementation counterparts.
This allows the signature help to display docstrings from the original
implementation.
2. It incorporates a more robust fix to a bug that was addressed in a
[previous PR](https://github.com/astral-sh/ruff/pull/19542). It also
adds more comprehensive tests to cover this case.

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-28 10:57:18 -07:00
Douglas Creager
130d4e1135 [ty] Don't panic with argument that doesn't actually implement Iterable (#19602)
This eliminates the panic reported in
https://github.com/astral-sh/ty/issues/909, though it doesn't address
the underlying cause, which is that we aren't yet checking the types of
the fields of a protocol when checking whether a class implements the
protocol. And in particular, if a class explictly opts out of iteration
via

```py
class NotIterable:
    __iter__ = None
```

we currently treat that as "having an `__iter__`" member, and therefore
implementing `Iterable`.

Note that the assumption that was in the comment before is still
correct: call binding will have already checked that the argument
satisfies `Iterable`, and so it shouldn't be an error to iterate over
said argument. But arguably, the new logic in this PR is a better way to
discharge that assumption — instead of panicking if we happen to be
wrong, fall back on an unknown iteration result.
2025-07-28 12:09:54 -04:00
Dan Parizher
e63dfa3d18 [flake8-commas] Add support for trailing comma checks in type parameter lists (COM812,COM819) (#19390)
## Summary

Fixes #18844

I'm not too sure if the solution is as simple as the way I implemented
it, but I'm curious to see if we are covering all cases correctly here.

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-07-28 10:53:04 -04:00
Junhson Jean-Baptiste
6d0f3ef3a5 [pylint] Implement auto-fix for missing-maxsplit-arg (PLC0207) (#19387)
<!--
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
As a follow-up to #18949 (suggested
[here](https://github.com/astral-sh/ruff/pull/18949#pullrequestreview-2998417889)),
this PR implements auto-fix logic for `PLC0207`.

## Test Plan

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

Existing tests pass, with updates to the snapshot so that it expects the
new output that comes along with the auto-fix.
2025-07-28 10:45:26 -04:00
Dan Parizher
201b079084 [refurb] Mark int and bool cases for Decimal.from_float as safe fixes in FURB164 tests (#19468)
## Summary

Fixes #19460

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-07-28 14:21:38 +00:00
David Peter
2680f2ed81 [ty] Minor: test isolation (#19597)
## Summary

Split the "Generator functions" tests into two parts. The first part
(synchronous) refers to a function called `i` from a function `i2`. But
`i` is later redeclared in the asynchronous part, which was probably not
intended.
2025-07-28 15:52:59 +02:00
Micha Reiser
afdfa042f3 [ty] Remove AssertUnwindSafe from BackgroundRequestHandler api (#19598) 2025-07-28 13:28:09 +00:00
Micha Reiser
8c0743df97 [ty] Fix "peek definition" in playground (#19592) 2025-07-28 09:13:00 +01:00
Dimitri Papadopoulos Orfanos
13634ff433 Use PEP 639 license information for Ruff itself instead of classifier (#19499)
## Summary

Declare licenses using only these two fields, as per PEP 639:
* `license`: SPDX license expression consisting of one or more license
identifiers
* `license-files`: list of license file glob patterns

Supported by maturin ≥ 1.9.0:
https://www.maturin.rs/changelog.html

## Test Plan

N/A
2025-07-28 09:43:50 +02:00
renovate[bot]
7a541f597f Update NPM Development dependencies (#19588)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 07:55:22 +01:00
renovate[bot]
2deb50f4e3 Update Rust crate get-size2 to 0.6.0 (#19590)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 07:55:03 +01:00
renovate[bot]
85e22645aa Update pre-commit hook astral-sh/ruff-pre-commit to v0.12.5 (#19585)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit)
| repository | patch | `v0.12.4` -> `v0.12.5` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

Note: The `pre-commit` manager in Renovate is not supported by the
`pre-commit` maintainers or community. Please do not report any problems
there, instead [create a Discussion in the Renovate
repository](https://redirect.github.com/renovatebot/renovate/discussions/new)
if you have any questions.

---

### Release Notes

<details>
<summary>astral-sh/ruff-pre-commit (astral-sh/ruff-pre-commit)</summary>

###
[`v0.12.5`](https://redirect.github.com/astral-sh/ruff-pre-commit/releases/tag/v0.12.5)

[Compare
Source](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.12.4...v0.12.5)

See: https://github.com/astral-sh/ruff/releases/tag/0.12.5

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS40MC4wIiwidXBkYXRlZEluVmVyIjoiNDEuNDAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 08:48:14 +02:00
renovate[bot]
d3f6de8b0e Update cargo-bins/cargo-binstall action to v1.14.2 (#19583)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[cargo-bins/cargo-binstall](https://redirect.github.com/cargo-bins/cargo-binstall)
| action | patch | `v1.14.1` -> `v1.14.2` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>cargo-bins/cargo-binstall (cargo-bins/cargo-binstall)</summary>

###
[`v1.14.2`](https://redirect.github.com/cargo-bins/cargo-binstall/releases/tag/v1.14.2)

[Compare
Source](https://redirect.github.com/cargo-bins/cargo-binstall/compare/v1.14.1...v1.14.2)

*Binstall is a tool to fetch and install Rust-based executables as
binaries. It aims to be a drop-in replacement for `cargo install` in
most cases. Install it today with `cargo install cargo-binstall`, from
the binaries below, or if you already have it, upgrade with `cargo
binstall cargo-binstall`.*

##### In this release:

- Upgrade dependencies

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS40MC4wIiwidXBkYXRlZEluVmVyIjoiNDEuNDAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 08:47:33 +02:00
renovate[bot]
9eb8174209 Update CodSpeedHQ/action action to v3.8.0 (#19587)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [CodSpeedHQ/action](https://redirect.github.com/CodSpeedHQ/action) |
action | minor | `v3.7.0` -> `v3.8.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>CodSpeedHQ/action (CodSpeedHQ/action)</summary>

###
[`v3.8.0`](https://redirect.github.com/CodSpeedHQ/action/releases/tag/v3.8.0)

[Compare
Source](https://redirect.github.com/CodSpeedHQ/action/compare/v3.7.0...v3.8.0)

##### What's Changed

##### <!-- 1 -->🐛 Bug Fixes

- Adjust offset for symbols of module loaded at preferred base by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias) in
[#&#8203;97](https://redirect.github.com/CodSpeedHQ/runner/pull/97)
- Run with --scope to allow perf to trace the benchmark process by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias)
- Run with bash to support complex scripts by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias)
- Execute pre- and post-bench scripts for non-perf walltime runner by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias) in
[#&#8203;96](https://redirect.github.com/CodSpeedHQ/runner/pull/96)

##### <!-- 2 -->🏗️ Refactor

- Process memory mappings in separate function by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias)

##### <!-- 7 -->⚙️ Internals

- Add debug logs for perf.map collection by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias)
- Add complex cmd and env tests by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias)

**Full Changelog**:
https://github.com/CodSpeedHQ/action/compare/v3.7.0...v3.8.0
**Full Runner Changelog**:
https://github.com/CodSpeedHQ/runner/blob/main/CHANGELOG.md

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS40MC4wIiwidXBkYXRlZEluVmVyIjoiNDEuNDAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 08:47:05 +02:00
renovate[bot]
9c68616d91 Update Rust crate criterion to 0.7.0 (#19589)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [criterion](https://bheisler.github.io/criterion.rs/book/index.html)
([source](https://redirect.github.com/bheisler/criterion.rs)) |
workspace.dependencies | minor | `0.6.0` -> `0.7.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>bheisler/criterion.rs (criterion)</summary>

###
[`v0.7.0`](https://redirect.github.com/bheisler/criterion.rs/blob/HEAD/CHANGELOG.md#070---2025-07-25)

[Compare
Source](https://redirect.github.com/bheisler/criterion.rs/compare/0.6.0...0.7.0)

- Bump version of criterion-plot to align dependencies.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS40MC4wIiwidXBkYXRlZEluVmVyIjoiNDEuNDAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 08:35:36 +02:00
renovate[bot]
9280c7e945 Update astral-sh/setup-uv action to v6.4.3 (#19586)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) |
action | minor | `v6.3.1` -> `v6.4.3` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>astral-sh/setup-uv (astral-sh/setup-uv)</summary>

###
[`v6.4.3`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.4.3):
🌈 fix relative paths starting with dots

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v6.4.2...v6.4.3)

#### 🐛 Bug fixes

- fix relative paths starting with dots
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;500](https://redirect.github.com/astral-sh/setup-uv/issues/500))

###
[`v6.4.2`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.4.2):
🌈 Interpret relative inputs as under working-directory

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v6.4.1...v6.4.2)

#### Changes

This release will interpret relative paths in inputs as relative
to the value of `working-directory` (default is `${{ github.workspace
}}`) .
This means the following configuration

```yaml
- uses: astral-sh/setup-uv@v6
   with:
     working-directory: /my/path
     cache-dependency-glob: uv.lock
```

will look for the `cache-dependency-glob` under `/my/path/uv.lock`

#### 🐛 Bug fixes

- interpret relative inputs as under working-directory
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;498](https://redirect.github.com/astral-sh/setup-uv/issues/498))

#### 🧰 Maintenance

- chore: update known versions for 0.8.1/0.8.2
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;497](https://redirect.github.com/astral-sh/setup-uv/issues/497))
- chore: update known versions for 0.8.0
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;491](https://redirect.github.com/astral-sh/setup-uv/issues/491))

###
[`v6.4.1`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.4.1):
🌈 Hotfix: Ignore deps starting with uv when finding uv version

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v6.4.0...v6.4.1)

##### Changes

Thank you [@&#8203;phpmypython](https://redirect.github.com/phpmypython)
for raising a PR to fix this issue!

##### 🐛 Bug fixes

- Ignore deps starting with uv when finding uv version
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;492](https://redirect.github.com/astral-sh/setup-uv/issues/492))

###
[`v6.4.0`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.4.0):
🌈 Add input `version-file`

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v6.3.1...v6.4.0)

##### Changes

You can now use the `version-file` input to specify a file that contains
the version of uv to install.
This can either be a `pyproject.toml` or `uv.toml` file which defines a
`required-version` or
uv defined as a dependency in `pyproject.toml` or `requirements.txt`.

```yaml
- name: Install uv based on the version defined in requirements.txt
  uses: astral-sh/setup-uv@v6
  with:
    version-file: "requirements.txt"
```

##### 🚀 Enhancements

- Add input version-file
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;486](https://redirect.github.com/astral-sh/setup-uv/issues/486))

##### 🧰 Maintenance

- chore: update known versions for 0.7.22
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;488](https://redirect.github.com/astral-sh/setup-uv/issues/488))
- Bump dependencies
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;487](https://redirect.github.com/astral-sh/setup-uv/issues/487))
- chore: update known versions for 0.7.21
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;483](https://redirect.github.com/astral-sh/setup-uv/issues/483))
- chore: update known versions for 0.7.20
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;480](https://redirect.github.com/astral-sh/setup-uv/issues/480))
- chore: update known versions for 0.7.19
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;475](https://redirect.github.com/astral-sh/setup-uv/issues/475))
- chore: update known versions for 0.7.18
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;473](https://redirect.github.com/astral-sh/setup-uv/issues/473))
- chore: update known versions for 0.7.17
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;468](https://redirect.github.com/astral-sh/setup-uv/issues/468))
- chore: update known versions for 0.7.16
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;466](https://redirect.github.com/astral-sh/setup-uv/issues/466))
- chore: update known versions for 0.7.15
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;463](https://redirect.github.com/astral-sh/setup-uv/issues/463))

##### 📚 Documentation

- Add FAQ on changed cache and cache upload behavior
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;477](https://redirect.github.com/astral-sh/setup-uv/issues/477))

##### ⬆️ Dependency updates

- Bump dependencies
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;487](https://redirect.github.com/astral-sh/setup-uv/issues/487))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS40MC4wIiwidXBkYXRlZEluVmVyIjoiNDEuNDAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 08:33:44 +02:00
Dhruv Manilawala
e19145040f [ty] Add workflow to comment conformance tests diff (#19555)
## Summary

Follow-up to https://github.com/astral-sh/ruff/pull/19556, this PR adds
the workflow that computes the diagnostic diff which the workflow
introduced in the linked PR will add as a comment.

This workflow is similar to the [ty ecosystem-analyzer
workflow](d781a6ab3f/.github/workflows/ty-ecosystem-analyzer.yaml).

Closes: astral-sh/ty#212

## Test Plan

1. Initially there's no diff to show
2. This
[commit](d0db9937df)
comments out a rule which updates the comment with the diff
3. Later, that commit is reverted and the diff goes away

Use the comment history to look at the diff output where the order of
the history corresponds to the steps mentioned above in reverse order
i.e., the edit in the middle will contain the diff output:

<img width="1082" height="313" alt="Screenshot 2025-07-25 at 21 09 26"
src="https://github.com/user-attachments/assets/6aceb60c-1987-4b9a-9063-e3999844f035"
/>
2025-07-28 11:33:22 +05:30
renovate[bot]
ef3a195f28 Update dependency ruff to v0.12.5 (#19584) 2025-07-27 22:22:23 -04:00
Dylan
008bbfdf5a Disallow implicit concatenation of t-strings and other string types (#19485)
As of [this cpython PR](https://github.com/python/cpython/pull/135996),
it is not allowed to concatenate t-strings with non-t-strings,
implicitly or explicitly. Expressions such as `"foo" t"{bar}"` are now
syntax errors.

This PR updates some AST nodes and parsing to reflect this change.

The structural change is that `TStringPart` is no longer needed, since,
as in the case of `BytesStringLiteral`, the only possibilities are that
we have a single `TString` or a vector of such (representing an implicit
concatenation of t-strings). This removes a level of nesting from many
AST expressions (which is what all the snapshot changes reflect), and
simplifies some logic in the implementation of visitors, for example.

The other change of note is in the parser. When we meet an implicit
concatenation of string-like literals, we now count the number of
t-string literals. If these do not exhaust the total number of
implicitly concatenated pieces, then we emit a syntax error. To recover
from this syntax error, we encode any t-string pieces as _invalid_
string literals (which means we flag them as invalid, record their
range, and record the value as `""`). Note that if at least one of the
pieces is an f-string we prefer to parse the entire string as an
f-string; otherwise we parse it as a string.

This logic is exactly the same as how we currently treat
`BytesStringLiteral` parsing and error recovery - and carries with it
the same pros and cons.

Finally, note that I have not implemented any changes in the
implementation of the formatter. As far as I can tell, none are needed.
I did change a few of the fixtures so that we are always concatenating
t-strings with t-strings.
2025-07-27 12:41:03 +00:00
Alex Waygood
df5eba7583 [ty] Mark all_type_assignable_to_iterable_are_iterable as flaky (#19574) 2025-07-27 11:04:13 +00:00
Micha Reiser
469c50b0b7 [ty] Support stdlib files in playground (#19557) 2025-07-26 19:33:38 +01:00
UnboundVariable
738246627f [ty] Implemented support for "selection range" language server feature (#19567)
This PR adds support for the "selection range" language server feature.
This feature was recently requested by a ty user in [this feature
request](https://github.com/astral-sh/ty/issues/882).

This feature allows a client to implement "smart selection expansion"
based on the structure of the parse tree. For example, if you type
"shift-ctrl-right-arrow" in VS Code, the current selection will be
expanded to include the parent AST node. Conversely,
"shift-ctrl-left-arrow" shrinks the selection.

We will probably need to tune the granularity of selection expansion
based on user feedback. The initial implementation includes most AST
nodes, but users may find this to be too fine-grained. We have the
option of skipping some AST nodes that are not as meaningful when
editing code.

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-26 09:08:36 -07:00
Douglas Creager
e867830848 [ty] Don't include already-bound legacy typevars in function generic context (#19558)
We now correctly exclude legacy typevars from enclosing scopes when
constructing the generic context for a generic function.

more detail:

A function is generic if it refers to legacy typevars in its signature:

```py
from typing import TypeVar

T = TypeVar("T")

def f(t: T) -> T:
    return t
```

Generic functions are allowed to appear inside of other generic
contexts. When they do, they can refer to the typevars of those
enclosing generic contexts, and that should not rebind the typevar:

```py
from typing import TypeVar, Generic

T = TypeVar("T")
U = TypeVar("U")

class C(Generic[T]):
    @staticmethod
    def method(t: T, u: U) -> None: ...

# revealed: def method(t: int, u: U) -> None
reveal_type(C[int].method)
```

This substitution was already being performed correctly, but we were
also still including the enclosing legacy typevars in the method's own
generic context, which can be seen via `ty_extensions.generic_context`
(which has been updated to work on generic functions and methods):

```py
from ty_extensions import generic_context

# before: tuple[T, U]
# after: tuple[U]
reveal_type(generic_context(C[int].method))
```

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-07-25 18:14:19 -04:00
Elliot Simpson
72fdb7d439 [flake8-blind-except] Change BLE001 to permit logging.critical(..., exc_info=True). (#19520)
## Summary

Changing `BLE001` (blind-except) so that it does not flag `except`
clauses which include `logging.critical(..., exc_info=True)`.

## Test Plan

It passes the following (whereas the `main` branch does not):
```sh
$ cargo run -p ruff -- check somefile.py --no-cache --select=BLE001
```
```python
# somefile.py

import logging


try:
    print("Hello world!")
except Exception:
    logging.critical("Did not run.", exc_info=True)
```
Related: https://github.com/astral-sh/ruff/issues/19519
2025-07-25 17:52:58 -04:00
Dylan
fbf1dfc782 Reword preview warning for target-version Python 3.14 (#19563)
Small rewording to indicate that core development is done but that we
may add breaking changes.

Feel free to bikeshed!

Test:

```console
❯ echo "t''" | cargo run -p ruff -- check --no-cache --isolated --target-version py314 -
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
     Running `target/debug/ruff check --no-cache --isolated --target-version py314 -`
warning: Support for Python 3.14 is in preview and may undergo breaking changes. Enable `preview` to remove this warning.
All checks passed!
```
2025-07-25 16:09:45 -05:00
UnboundVariable
a0d8ff51dd [ty] Added support for "document symbols" and "workspace symbols" (#19521)
This PR adds support for "document symbols" and "workspace symbols"
language server features. Most of the logic to implement these features
is shared.

The "document symbols" feature returns a list of all symbols within a
specified source file. Clients can specify whether they want a flat or
hierarchical list. Document symbols are typically presented by a client
in an "outline" form. Here's what this looks like in VS Code, for
example.

<img width="240" height="249" alt="image"
src="https://github.com/user-attachments/assets/82b11f4f-32ec-4165-ba01-d6496ad13bdf"
/>


The "workspace symbols" feature returns a list of all symbols across the
entire workspace that match some user-supplied query string. This allows
the user to quickly find and navigate to any symbol within their code.

<img width="450" height="134" alt="image"
src="https://github.com/user-attachments/assets/aac131e0-9464-4adf-8a6c-829da028c759"
/>

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-25 13:07:38 -07:00
Brent Westbrook
165091a31c Add TextEmitter::with_color and disable colors in unreadable_files test (#19562)
Summary
--

I looked at other uses of `TextEmitter`, and I think this should be the
only one affected by this. The other integration tests must work
properly since they're run with `assert_cmd_snapshot!`, which I assume
triggers the `SHOULD_COLORIZE` case, and the `cfg!(test)` check will
work for uses in `ruff_linter`.


4a4dc38b5b/crates/ruff_linter/src/message/text.rs (L36-L44)

Alternatively, we could probably move this to a CLI test instead.

Test Plan
--

`cargo test -p ruff`, which was failing on `main` with color codes in
the output before this
2025-07-25 15:47:49 -04:00
UnboundVariable
4a4dc38b5b [ty] Added support for document highlights in playground. (#19540)
This PR adds support for the "document highlights" feature in the ty
playground.

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-25 08:55:40 -07:00
Dan Parizher
3e366fdf13 [refurb] Ignore decorated functions for FURB118 (#19339)
## Summary

Fixes #19305
2025-07-25 10:43:17 -05:00
Dhruv Manilawala
53e9e4421c [ty] Add workflow to comment diagnostic diff for conformance tests (#19556)
## Summary

This PR adds a workflow to comment the diff of diagnostics when running
ty between `main` and a pull request on the [typing conformance test
suite](https://github.com/python/typing/tree/main/conformance/tests).

The main workflow is introduced in
https://github.com/astral-sh/ruff/pull/19555 which this workflow depends
on. This workflow is similar to the [mypy primer
comment](d781a6ab3f/.github/workflows/mypy_primer_comment.yaml)
workflow.

## Test Plan

I cannot test this workflow without merging it on `main` unless anyone
knows a way to do this.
2025-07-25 20:54:28 +05:30
Alex Waygood
859262bd49 [ty] Move zope.interface to good.txt for primer runs (#19208) 2025-07-25 14:12:17 +01:00
David Peter
c0768dfd96 [ty] Attribute access on intersections with negative parts (#19524)
## Summary

We currently infer a `@Todo` type whenever we access an attribute on an
intersection type with negative components. This can happen very
naturally. Consequently, this `@Todo` type is rather pervasive and hides
a lot of true positives that ty could otherwise detect:

```py
class Foo:
    attr: int = 1

def _(f: Foo | None):
    if f:
        reveal_type(f)  # Foo & ~AlwaysFalsy

        reveal_type(f.attr)  # now: int, previously: @Todo
```

The changeset here proposes to handle member access on these
intersection types by simply ignoring all negative contributions. This
is not always ideal: a negative contribution like `~<Protocol with
members 'attr'>` could be a hint that `.attr` should not be accessible
on the full intersection type. The behavior can certainly be improved in
the future, but this seems like a reasonable initial step to get rid of
this unnecessary `@Todo` type.

## Ecosystem analysis

There are quite a few changes here. I spot-checked them and found one
bug where attribute access on pure negation types (`~P == object & ~P`)
would not allow attributes on `object` to be accessed. After that was
fixed, I only see true positives and known problems. The fact that a lot
of `unused-ignore-comment` diagnostics go away are also evidence for the
fact that this touches a sensitive area, where static analysis clashes
with dynamically adding attributes to objects:
```py
… # type: ignore # Runtime attribute access
```

## Test Plan

Updated tests.
2025-07-25 14:56:14 +02:00
David Peter
d4eb4277ad [ty] Add basic support for dataclasses.field (#19553)
## Summary

Add basic support for `dataclasses.field`:
* remove fields with `init=False` from the signature of the synthesized
`__init__` method
* infer correct default value types from `default` or `default_factory`
arguments

```py
from dataclasses import dataclass, field

def default_roles() -> list[str]:
    return ["user"]

@dataclass
class Member:
    name: str
    roles: list[str] = field(default_factory=default_roles)
    tag: str | None = field(default=None, init=False)

# revealed: (self: Member, name: str, roles: list[str] = list[str]) -> None
reveal_type(Member.__init__)
```

Support for `kw_only` has **not** been added.

part of https://github.com/astral-sh/ty/issues/111

## Test Plan

New Markdown tests
2025-07-25 14:56:04 +02:00
Micha Reiser
b033fb6bfd [ty] Split ScopedPlaceId into ScopedSymbolId and ScopedMemberId (#19497) 2025-07-25 13:54:33 +02:00
Alex Waygood
f722bfa9e6 [ty] Do not consider a type T to satisfy a method member on a protocol unless the method is available on the meta-type of T (#19187) 2025-07-25 11:16:04 +01:00
Shunsuke Shibayama
b124e182ca [ty] improve lazy scope place lookup (#19321)
Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
Co-authored-by: Carl Meyer <carl@oddbird.net>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-07-25 07:11:11 +00:00
Dhruv Manilawala
57373a7e4d [ty] Derive Serialize unconditionally on client options (#19549) 2025-07-25 04:03:03 +00:00
Carl Meyer
ae9d450b5f [ty] Fallback to Unknown if no type is stored for an expression (#19517)
## Summary

See discussion at
https://github.com/astral-sh/ruff/pull/19478/files#r2223870292

Fixes https://github.com/astral-sh/ty/issues/865

## Test Plan

Added one mdtest for invalid Callable annotation; removed `pull-types:
skip` from that test file.

Co-authored-by: lipefree <willy.ngo.2000@gmail.com>
2025-07-25 02:05:32 +00:00
UnboundVariable
c8c80e054e [ty] Fix bug #879 in signature help (#19542)
This PR fixes bug [#879](https://github.com/astral-sh/ty/issues/879)
where the signature help popup remains visible after typing the closing
paren in a call expression.

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-24 16:26:14 -07:00
UnboundVariable
4bc34b82ef [ty] Added support for "document highlights" language server feature. (#19515)
This PR adds support for the "document highlights" language server
feature.

This feature allows a client to highlight all instances of a selected
name within a document. Without this feature, editors perform
highlighting based on a simple text match. This adds semantic knowledge.

The implementation of this feature largely overlaps that of the
recently-added "references" feature. This PR refactors the existing
"references.rs" module, separating out the functionality and tests that
are specific to the other language feature into a "goto_references.rs"
module. The "references.rs" module now contains the functionality that
is common to "goto references", "document highlights" and "rename"
(which is not yet implemented).

As part of this PR, I also created a new `ReferenceTarget` type which is
similar to the existing `NavigationTarget` type but better suited for
references. This idea was suggested by @MichaReiser in [this code review
feedback](https://github.com/astral-sh/ruff/pull/19475#discussion_r2224061006)
from a previous PR. Notably, this new type contains a field that
specifies the "kind" of the reference (read, write or other). This
"kind" is needed for the document highlights feature.

Before: all textual instances of `foo` are highlighted
<img width="156" height="126" alt="Screenshot 2025-07-23 at 12 51 09 PM"
src="https://github.com/user-attachments/assets/37ccdb2f-d48a-473d-89d5-8e89cb6c394e"
/>

After: only semantic matches are highlighted
<img width="164" height="157" alt="Screenshot 2025-07-23 at 12 52 05 PM"
src="https://github.com/user-attachments/assets/2efadadd-4691-4815-af04-b031e74c81b7"
/>

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-24 13:06:25 -07:00
Charlie Marsh
d9cab4d242 Add support for specifying minimum dots in detected string imports (#19538)
## Summary

Defaults to requiring two dots, which matches the Pants default.
2025-07-24 15:48:23 -04:00
David Peter
d77b7312b0 [ty] Minor: fix incomplete docstring (#19534) 2025-07-24 21:01:15 +02:00
Dhruv Manilawala
f9091ea8bb [ty] Move server tests as integration tests (#19522)
## Summary

Reference:
https://github.com/astral-sh/ruff/pull/19391#discussion_r2222780892
2025-07-24 16:10:17 +00:00
Robsdedude
1d2181623c [ruff] Offer fixes for RUF039 in more cases (#19065)
## Summary
Expand cases in which ruff can offer a fix for `RUF039` (some of which
are unsafe).

While turning `"\n"` (== `\n`) into `r"\n"` (== `\\n`) is not equivalent
at run-time, it's still functionally equivalent to do so in the context
of [regex
patterns](https://docs.python.org/3/library/re.html#regular-expression-syntax)
as they themselves interpret the escape sequence. Therefore, an unsafe
fix can be offered.

Further, this PR also makes ruff offer fixes for byte string literals,
not only strings literals as before.

## Test Plan
Tests for all escape sequences have been added.

## Related
Closes: https://github.com/astral-sh/ruff/issues/16713

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-07-24 11:45:45 -04:00
David Peter
dc6be457b5 [ty] Support dataclasses.InitVar (#19527)
## Summary

I saw that this creates a lot of false positives in the ecosystem, and
it seemed to be relatively easy to add basic support for this.

Some preliminary work on this was done by @InSyncWithFoo — thank you.

part of https://github.com/astral-sh/ty/issues/111

## Ecosystem analysis

The results look good.

## Test Plan

New Markdown tests

---------

Co-authored-by: InSync <insyncwithfoo@gmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-07-24 16:33:33 +02:00
Robsdedude
1079975b35 [ruff] Fix RUF033 breaking with named default expressions (#19115)
## Summary
The generated fix for `RUF033` would cause a syntax error for named
expressions as parameter defaults.
```python
from dataclasses import InitVar, dataclass
@dataclass
class Foo:
    def __post_init__(self, bar: int = (x := 1)) -> None:
        pass
```
would be turned into
```python
from dataclasses import InitVar, dataclass
@dataclass
class Foo:
    x: InitVar[int] = x := 1
    def __post_init__(self, bar: int = (x := 1)) -> None:
        pass
```
instead of the syntactically correct
```python
# ...
x: InitVar[int] = (x := 1)
# ...
```

## Test Plan
Test reproducer (plus some extra tests) have been added to the test
suite.

## Related
Fixes: https://github.com/astral-sh/ruff/issues/18950
2025-07-24 09:45:49 -04:00
Brent Westbrook
39eb0f6c6c Update pre-commit hook name (#19530)
## Summary

A couple of months ago now
(https://github.com/astral-sh/ruff-pre-commit/pull/124) we changed the
hook ID from just `ruff` to `ruff-check` to mirror `ruff-format`. I
noticed the `ruff (legacy alias)` when running pre-commit on the release
today and realized we should probably update.

## Test Plan

Commit on this PR:

```shell
> git commit -m "Update pre-commit hook name"
check for merge conflicts................................................Passed
Validate pyproject.toml..............................(no files to check)Skipped
mdformat.............................................(no files to check)Skipped
markdownlint-fix.....................................(no files to check)Skipped
blacken-docs.........................................(no files to check)Skipped
typos....................................................................Passed
cargo fmt............................................(no files to check)Skipped
ruff format..........................................(no files to check)Skipped
ruff check...........................................(no files to check)Skipped  <-- 
prettier.................................................................Passed
zizmor...............................................(no files to check)Skipped
Validate GitHub Workflows............................(no files to check)Skipped
shellcheck...........................................(no files to check)Skipped
```

Compared to the release branch:

```shell
> pre-commit run
...
cargo fmt............................................(no files to check)Skipped
ruff format..........................................(no files to check)Skipped
ruff (legacy alias)..................................(no files to check)Skipped
...
```
2025-07-24 09:44:47 -04:00
456 changed files with 19395 additions and 11264 deletions

View File

@@ -429,7 +429,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1
uses: cargo-bins/cargo-binstall@808dcb1b503398677d089d3216c51ac7cc11e7ab # v1.14.2
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -451,7 +451,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download Ruff binary to test
id: download-cached-binary
@@ -652,7 +652,7 @@ jobs:
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: Fuzz
env:
FORCE_COLOR: 1
@@ -682,7 +682,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1
- uses: cargo-bins/cargo-binstall@808dcb1b503398677d089d3216c51ac7cc11e7ab # v1.14.2
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -722,7 +722,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
@@ -765,7 +765,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -897,7 +897,7 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: "Install Rust toolchain"
run: rustup show
@@ -911,7 +911,7 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@c28fe9fbe7d57a3da1b7834ae3761c1d8217612d # v3.7.0
uses: CodSpeedHQ/action@0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b # v3.8.0
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
@@ -930,7 +930,7 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: "Install Rust toolchain"
run: rustup show
@@ -944,7 +944,7 @@ jobs:
run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@c28fe9fbe7d57a3da1b7834ae3761c1d8217612d # v3.7.0
uses: CodSpeedHQ/action@0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b # v3.8.0
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"

View File

@@ -38,7 +38,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
@@ -81,7 +81,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: wheels-*

View File

@@ -65,7 +65,7 @@ jobs:
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: Sync typeshed stubs
run: |
rm -rf "ruff/${VENDORED_TYPESHED}"
@@ -117,7 +117,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -155,7 +155,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: Setup git
run: |
git config --global user.name typeshedbot

View File

@@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:

View File

@@ -29,7 +29,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:

View File

@@ -0,0 +1,109 @@
name: Run typing conformance
permissions: {}
on:
pull_request:
paths:
- "crates/ty*/**"
- "crates/ruff_db"
- "crates/ruff_python_ast"
- "crates/ruff_python_parser"
- ".github/workflows/typing_conformance.yaml"
- ".github/workflows/typing_conformance_comment.yaml"
- "Cargo.lock"
- "!**.md"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
jobs:
typing_conformance:
name: Compute diagnostic diff
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: python/typing
ref: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc
path: typing
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
workspaces: "ruff"
- name: Install Rust toolchain
run: rustup show
- name: Compute diagnostic diff
shell: bash
run: |
RUFF_DIR="$GITHUB_WORKSPACE/ruff"
# Build the executable for the old and new commit
(
cd ruff
echo "new commit"
git checkout -b new_commit "${{ github.event.pull_request.head.sha }}"
git rev-list --format=%s --max-count=1 new_commit
cargo build --release --bin ty
mv target/release/ty ty-new
echo "old commit (merge base)"
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
git checkout -b old_commit "$MERGE_BASE"
git rev-list --format=%s --max-count=1 old_commit
cargo build --release --bin ty
mv target/release/ty ty-old
)
(
cd typing/conformance/tests
echo "Running ty on old commit (merge base)"
"$RUFF_DIR/ty-old" check --color=never --output-format=concise . > "$GITHUB_WORKSPACE/old-output.txt" 2>&1 || true
echo "Running ty on new commit"
"$RUFF_DIR/ty-new" check --color=never --output-format=concise . > "$GITHUB_WORKSPACE/new-output.txt" 2>&1 || true
)
if ! diff -u old-output.txt new-output.txt > typing_conformance_diagnostics.diff; then
echo "Differences found between base and PR"
else
echo "No differences found"
touch typing_conformance_diagnostics.diff
fi
echo ${{ github.event.number }} > pr-number
- name: Upload diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: typing_conformance_diagnostics_diff
path: typing_conformance_diagnostics.diff
- name: Upload pr-number
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: pr-number
path: pr-number

View File

@@ -0,0 +1,97 @@
name: PR comment (typing_conformance)
on: # zizmor: ignore[dangerous-triggers]
workflow_run:
workflows: [Run typing conformance]
types: [completed]
workflow_dispatch:
inputs:
workflow_run_id:
description: The typing_conformance workflow that triggers the workflow run
required: true
jobs:
comment:
runs-on: ubuntu-24.04
permissions:
pull-requests: write
steps:
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: Download PR number
with:
name: pr-number
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
if_no_artifact_found: ignore
allow_forks: true
- name: Parse pull request number
id: pr-number
run: |
if [[ -f pr-number ]]
then
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: "Download typing_conformance results"
id: download-typing_conformance_diff
if: steps.pr-number.outputs.pr-number
with:
name: typing_conformance_diagnostics_diff
workflow: typing_conformance.yaml
pr: ${{ steps.pr-number.outputs.pr-number }}
path: pr/typing_conformance_diagnostics_diff
workflow_conclusion: completed
if_no_artifact_found: ignore
allow_forks: true
- name: Generate comment content
id: generate-comment
if: ${{ steps.download-typing_conformance_diff.outputs.found_artifact == 'true' }}
run: |
# Guard against malicious typing_conformance results that symlink to a secret
# file on this runner
if [[ -L pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff ]]
then
echo "Error: typing_conformance_diagnostics.diff cannot be a symlink"
exit 1
fi
# Note this identifier is used to find the comment to update on
# subsequent runs
echo '<!-- generated-comment typing_conformance_diagnostics_diff -->' >> comment.txt
echo '## Diagnostic diff on typing conformance tests' >> comment.txt
if [ -s "pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff" ]; then
echo '<details>' >> comment.txt
echo '<summary>Changes were detected when running ty on typing conformance tests</summary>' >> comment.txt
echo '' >> comment.txt
echo '```diff' >> comment.txt
cat pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff >> comment.txt
echo '```' >> comment.txt
echo '</details>' >> comment.txt
else
echo 'No changes detected when running ty on typing conformance tests ✅' >> comment.txt
fi
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
cat comment.txt >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
if: steps.generate-comment.outcome == 'success'
id: find-comment
with:
issue-number: ${{ steps.pr-number.outputs.pr-number }}
comment-author: "github-actions[bot]"
body-includes: "<!-- generated-comment typing_conformance_diagnostics_diff -->"
- name: Create or update comment
if: steps.find-comment.outcome == 'success'
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.pr-number }}
body-path: comment.txt
edit-mode: replace

View File

@@ -81,10 +81,10 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.4
rev: v0.12.5
hooks:
- id: ruff-format
- id: ruff
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
types_or: [python, pyi]
require_serial: true

29
Cargo.lock generated
View File

@@ -530,7 +530,7 @@ dependencies = [
"ciborium",
"clap",
"codspeed",
"criterion-plot",
"criterion-plot 0.5.0",
"is-terminal",
"itertools 0.10.5",
"num-traits",
@@ -713,15 +713,15 @@ dependencies = [
[[package]]
name = "criterion"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"criterion-plot 0.6.0",
"itertools 0.13.0",
"num-traits",
"oorandom",
@@ -742,6 +742,16 @@ dependencies = [
"itertools 0.10.5",
]
[[package]]
name = "criterion-plot"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
dependencies = [
"cast",
"itertools 0.13.0",
]
[[package]]
name = "crossbeam"
version = "0.8.4"
@@ -1151,9 +1161,9 @@ dependencies = [
[[package]]
name = "get-size-derive2"
version = "0.5.2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "028f3cfad7c3e3b1d8d04ef0a1c03576f2d62800803fe1301a4cd262849f2dea"
checksum = "ca171f9f8ed2f416ac044de2dc4acde3e356662a14ac990345639653bdc7fc28"
dependencies = [
"attribute-derive",
"quote",
@@ -1162,9 +1172,9 @@ dependencies = [
[[package]]
name = "get-size2"
version = "0.5.2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a09c2043819a3def7bfbb4927e7df96aab0da4cfd8824484b22d0c94e84458e"
checksum = "965bc5c1c5fe05c5bbd398bb9b3f0f14d750261ebdd1af959f2c8a603fedb5ad"
dependencies = [
"compact_str",
"get-size-derive2",
@@ -2881,6 +2891,7 @@ dependencies = [
"tracing",
"tracing-subscriber",
"ty_static",
"unicode-width 0.2.1",
"web-time",
"zip",
]
@@ -2960,6 +2971,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"memchr",
"ruff_cache",
"ruff_db",
"ruff_linter",
@@ -4304,7 +4316,6 @@ dependencies = [
"strum_macros",
"tempfile",
"test-case",
"thin-vec",
"thiserror 2.0.12",
"tracing",
"ty_python_semantic",

View File

@@ -73,7 +73,7 @@ console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version = "3.0.1" }
compact_str = "0.9.0"
criterion = { version = "0.6.0", default-features = false }
criterion = { version = "0.7.0", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "6.0.1" }
dir-test = { version = "0.4.0" }
@@ -83,7 +83,7 @@ etcetera = { version = "0.10.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }
get-size2 = { version = "0.5.0", features = [
get-size2 = { version = "0.6.0", features = [
"derive",
"smallvec",
"hashbrown",
@@ -166,7 +166,6 @@ strum_macros = { version = "0.27.0" }
syn = { version = "2.0.55" }
tempfile = { version = "3.9.0" }
test-case = { version = "3.3.1" }
thin-vec = { version = "0.2.14" }
thiserror = { version = "2.0.0" }
tikv-jemallocator = { version = "0.6.0" }
toml = { version = "0.9.0" }

View File

@@ -169,6 +169,9 @@ pub struct AnalyzeGraphCommand {
/// Attempt to detect imports from string literals.
#[clap(long)]
detect_string_imports: bool,
/// The minimum number of dots in a string import to consider it a valid import.
#[clap(long)]
min_dots: Option<usize>,
/// Enable preview mode. Use `--no-preview` to disable.
#[arg(long, overrides_with("no_preview"))]
preview: bool,
@@ -808,6 +811,7 @@ impl AnalyzeGraphCommand {
} else {
None
},
string_imports_min_dots: self.min_dots,
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
target_version: self.target_version.map(ast::PythonVersion::from),
..ExplicitConfigOverrides::default()
@@ -1305,6 +1309,7 @@ struct ExplicitConfigOverrides {
show_fixes: Option<bool>,
extension: Option<Vec<ExtensionPair>>,
detect_string_imports: Option<bool>,
string_imports_min_dots: Option<usize>,
}
impl ConfigurationTransformer for ExplicitConfigOverrides {
@@ -1392,6 +1397,9 @@ impl ConfigurationTransformer for ExplicitConfigOverrides {
if let Some(detect_string_imports) = &self.detect_string_imports {
config.analyze.detect_string_imports = Some(*detect_string_imports);
}
if let Some(string_imports_min_dots) = &self.string_imports_min_dots {
config.analyze.string_imports_min_dots = Some(*string_imports_min_dots);
}
config
}

View File

@@ -102,7 +102,7 @@ pub(crate) fn analyze_graph(
// Resolve the per-file settings.
let settings = resolver.resolve(path);
let string_imports = settings.analyze.detect_string_imports;
let string_imports = settings.analyze.string_imports;
let include_dependencies = settings.analyze.include_dependencies.get(path).cloned();
// Skip excluded files.

View File

@@ -279,6 +279,7 @@ mod test {
TextEmitter::default()
.with_show_fix_status(true)
.with_color(false)
.emit(
&mut output,
&diagnostics.inner,

View File

@@ -57,33 +57,40 @@ fn dependencies() -> Result<()> {
.write_str(indoc::indoc! {r#"
def f(): pass
"#})?;
root.child("ruff")
.child("e.pyi")
.write_str(indoc::indoc! {r#"
def f() -> None: ...
"#})?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().current_dir(&root), @r###"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": [
"ruff/d.py"
],
"ruff/d.py": [
"ruff/e.py"
],
"ruff/e.py": []
}
assert_cmd_snapshot!(command().current_dir(&root), @r#"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": [
"ruff/d.py"
],
"ruff/d.py": [
"ruff/e.py",
"ruff/e.pyi"
],
"ruff/e.py": [],
"ruff/e.pyi": []
}
----- stderr -----
"###);
----- stderr -----
"#);
});
Ok(())
@@ -197,23 +204,43 @@ fn string_detection() -> Result<()> {
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r###"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": []
}
assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r#"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [],
"ruff/c.py": []
}
----- stderr -----
"###);
----- stderr -----
"#);
});
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().arg("--detect-string-imports").arg("--min-dots").arg("1").current_dir(&root), @r#"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": []
}
----- stderr -----
"#);
});
Ok(())

View File

@@ -2422,7 +2422,7 @@ requires-python = ">= 3.11"
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.11
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -2734,7 +2734,7 @@ requires-python = ">= 3.11"
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.10
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -3098,7 +3098,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.11
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -3478,7 +3478,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.11
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -3806,7 +3806,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.10
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -4134,7 +4134,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.9
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -4419,7 +4419,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.9
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -4757,7 +4757,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.10
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}

View File

@@ -392,7 +392,7 @@ formatter.docstring_code_line_width = dynamic
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.7
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}

View File

@@ -42,6 +42,7 @@ serde_json = { workspace = true, optional = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, optional = true }
unicode-width = { workspace = true }
zip = { workspace = true }
[target.'cfg(target_arch="wasm32")'.dependencies]

View File

@@ -6,7 +6,9 @@ use ruff_source_file::{LineColumn, SourceCode, SourceFile};
use ruff_annotate_snippets::Level as AnnotateLevel;
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use self::render::{DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input};
pub use self::render::{
DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input, ceil_char_boundary,
};
use crate::{Db, files::File};
mod render;

View File

@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::path::Path;
@@ -7,7 +8,7 @@ use ruff_annotate_snippets::{
};
use ruff_notebook::{Notebook, NotebookIndex};
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
use ruff_text_size::{TextRange, TextSize};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::diagnostic::stylesheet::DiagnosticStylesheet;
use crate::{
@@ -520,7 +521,7 @@ impl<'r> RenderableSnippets<'r> {
#[derive(Debug)]
struct RenderableSnippet<'r> {
/// The actual snippet text.
snippet: &'r str,
snippet: Cow<'r, str>,
/// The absolute line number corresponding to where this
/// snippet begins.
line_start: OneIndexed,
@@ -580,6 +581,13 @@ impl<'r> RenderableSnippet<'r> {
.iter()
.map(|ann| RenderableAnnotation::new(snippet_start, ann))
.collect();
let EscapedSourceCode {
text: snippet,
annotations,
} = replace_whitespace_and_unprintable(snippet, annotations)
.fix_up_empty_spans_after_line_terminator();
RenderableSnippet {
snippet,
line_start,
@@ -590,7 +598,7 @@ impl<'r> RenderableSnippet<'r> {
/// Convert this to an "annotate" snippet.
fn to_annotate<'a>(&'a self, path: &'a str) -> AnnotateSnippet<'a> {
AnnotateSnippet::source(self.snippet)
AnnotateSnippet::source(&self.snippet)
.origin(path)
.line_start(self.line_start.get())
.annotations(
@@ -820,6 +828,230 @@ fn relativize_path<'p>(cwd: &SystemPath, path: &'p str) -> &'p str {
path
}
/// Given some source code and annotation ranges, this routine replaces tabs
/// with ASCII whitespace, and unprintable characters with printable
/// representations of them.
///
/// The source code and annotations returned are updated to reflect changes made
/// to the source code (if any).
fn replace_whitespace_and_unprintable<'r>(
source: &'r str,
mut annotations: Vec<RenderableAnnotation<'r>>,
) -> EscapedSourceCode<'r> {
// Updates the annotation ranges given by the caller whenever a single byte (at `index` in
// `source`) is replaced with `len` bytes.
//
// When the index occurs before the start of the range, the range is
// offset by `len`. When the range occurs after or at the start but before
// the end, then the end of the range only is offset by `len`.
let mut update_ranges = |index: usize, len: u32| {
for ann in &mut annotations {
if index < usize::from(ann.range.start()) {
ann.range += TextSize::new(len - 1);
} else if index < usize::from(ann.range.end()) {
ann.range = ann.range.add_end(TextSize::new(len - 1));
}
}
};
// If `c` is an unprintable character, then this returns a printable
// representation of it (using a fancier Unicode codepoint).
let unprintable_replacement = |c: char| -> Option<char> {
match c {
'\x07' => Some('␇'),
'\x08' => Some('␈'),
'\x1b' => Some('␛'),
'\x7f' => Some('␡'),
_ => None,
}
};
const TAB_SIZE: usize = 4;
let mut width = 0;
let mut column = 0;
let mut last_end = 0;
let mut result = String::new();
for (index, c) in source.char_indices() {
let old_width = width;
match c {
'\n' | '\r' => {
width = 0;
column = 0;
}
'\t' => {
let tab_offset = TAB_SIZE - (column % TAB_SIZE);
width += tab_offset;
column += tab_offset;
let tab_width =
u32::try_from(width - old_width).expect("small width because of tab size");
result.push_str(&source[last_end..index]);
update_ranges(result.text_len().to_usize(), tab_width);
for _ in 0..tab_width {
result.push(' ');
}
last_end = index + 1;
}
_ => {
width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
column += 1;
if let Some(printable) = unprintable_replacement(c) {
result.push_str(&source[last_end..index]);
let len = printable.text_len().to_u32();
update_ranges(result.text_len().to_usize(), len);
result.push(printable);
last_end = index + 1;
}
}
}
}
// No tabs or unprintable chars
if result.is_empty() {
EscapedSourceCode {
annotations,
text: Cow::Borrowed(source),
}
} else {
result.push_str(&source[last_end..]);
EscapedSourceCode {
annotations,
text: Cow::Owned(result),
}
}
}
struct EscapedSourceCode<'r> {
text: Cow<'r, str>,
annotations: Vec<RenderableAnnotation<'r>>,
}
impl<'r> EscapedSourceCode<'r> {
// This attempts to "fix up" the spans on each annotation in the case where
// it's an empty span immediately following a line terminator.
//
// At present, `annotate-snippets` (both upstream and our vendored copy)
// will render annotations of such spans to point to the space immediately
// following the previous line. But ideally, this should point to the space
// immediately preceding the next line.
//
// After attempting to fix `annotate-snippets` and giving up after a couple
// hours, this routine takes a different tact: it adjusts the span to be
// non-empty and it will cover the first codepoint of the following line.
// This forces `annotate-snippets` to point to the right place.
//
// See also: <https://github.com/astral-sh/ruff/issues/15509> and
// `ruff_linter::message::text::SourceCode::fix_up_empty_spans_after_line_terminator`,
// from which this was adapted.
fn fix_up_empty_spans_after_line_terminator(mut self) -> EscapedSourceCode<'r> {
for ann in &mut self.annotations {
let range = ann.range;
if !range.is_empty()
|| range.start() == TextSize::from(0)
|| range.start() >= self.text.text_len()
{
continue;
}
if !matches!(
self.text.as_bytes()[range.start().to_usize() - 1],
b'\n' | b'\r'
) {
continue;
}
let start = range.start();
let end = ceil_char_boundary(&self.text, start + TextSize::from(1));
ann.range = TextRange::new(start, end);
}
self
}
}
/// Finds the closest [`TextSize`] not less than the offset given for which
/// `is_char_boundary` is `true`. Unless the offset given is greater than
/// the length of the underlying contents, in which case, the length of the
/// contents is returned.
///
/// Can be replaced with `str::ceil_char_boundary` once it's stable.
///
/// # Examples
///
/// From `std`:
///
/// ```
/// use ruff_db::diagnostic::ceil_char_boundary;
/// use ruff_text_size::{Ranged, TextLen, TextSize};
///
/// let source = "❤️🧡💛💚💙💜";
/// assert_eq!(source.text_len(), TextSize::from(26));
/// assert!(!source.is_char_boundary(13));
///
/// let closest = ceil_char_boundary(source, TextSize::from(13));
/// assert_eq!(closest, TextSize::from(14));
/// assert_eq!(&source[..closest.to_usize()], "❤️🧡💛");
/// ```
///
/// Additional examples:
///
/// ```
/// use ruff_db::diagnostic::ceil_char_boundary;
/// use ruff_text_size::{Ranged, TextRange, TextSize};
///
/// let source = "Hello";
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(5)),
/// TextSize::from(5)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(6)),
/// TextSize::from(5)
/// );
///
/// let source = "α";
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(1)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(2)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(3)),
/// TextSize::from(2)
/// );
/// ```
pub fn ceil_char_boundary(text: &str, offset: TextSize) -> TextSize {
let upper_bound = offset
.to_u32()
.saturating_add(4)
.min(text.text_len().to_u32());
(offset.to_u32()..upper_bound)
.map(TextSize::from)
.find(|offset| text.is_char_boundary(offset.to_usize()))
.unwrap_or_else(|| TextSize::from(upper_bound))
}
#[cfg(test)]
mod tests {
@@ -2359,7 +2591,7 @@ watermelon
}
/// Returns a builder for tersely constructing diagnostics.
fn builder(
pub(super) fn builder(
&mut self,
identifier: &'static str,
severity: Severity,
@@ -2426,7 +2658,7 @@ watermelon
///
/// See the docs on `TestEnvironment::span` for the meaning of
/// `path`, `line_offset_start` and `line_offset_end`.
fn primary(
pub(super) fn primary(
mut self,
path: &str,
line_offset_start: &str,

View File

@@ -1,8 +1,8 @@
#[cfg(test)]
mod tests {
use crate::diagnostic::{
DiagnosticFormat,
render::tests::{create_diagnostics, create_syntax_error_diagnostics},
DiagnosticFormat, Severity,
render::tests::{TestEnvironment, create_diagnostics, create_syntax_error_diagnostics},
};
#[test]
@@ -63,4 +63,118 @@ mod tests {
|
");
}
/// Check that the new `full` rendering code in `ruff_db` handles cases fixed by commit c9b99e4.
///
/// For example, without the fix, we get diagnostics like this:
///
/// ```
/// error[no-indented-block]: Expected an indented block
/// --> example.py:3:1
/// |
/// 2 | if False:
/// | ^
/// 3 | print()
/// |
/// ```
///
/// where the caret points to the end of the previous line instead of the start of the next.
#[test]
fn empty_span_after_line_terminator() {
let mut env = TestEnvironment::new();
env.add(
"example.py",
r#"
if False:
print()
"#,
);
env.format(DiagnosticFormat::Full);
let diagnostic = env
.builder(
"no-indented-block",
Severity::Error,
"Expected an indented block",
)
.primary("example.py", "3:0", "3:0", "")
.build();
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[no-indented-block]: Expected an indented block
--> example.py:3:1
|
2 | if False:
3 | print()
| ^
|
");
}
/// Check that the new `full` rendering code in `ruff_db` handles cases fixed by commit 2922490.
///
/// For example, without the fix, we get diagnostics like this:
///
/// ```
/// error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
/// --> example.py:1:25
/// |
/// 1 | nested_fstrings = f'␈{f'{f'␛'}'}'
/// | ^
/// |
/// ```
///
/// where the caret points to the `f` in the f-string instead of the start of the invalid
/// character (`^Z`).
#[test]
fn unprintable_characters() {
let mut env = TestEnvironment::new();
env.add("example.py", "nested_fstrings = f'{f'{f''}'}'");
env.format(DiagnosticFormat::Full);
let diagnostic = env
.builder(
"invalid-character-sub",
Severity::Error,
r#"Invalid unescaped character SUB, use "\x1A" instead"#,
)
.primary("example.py", "1:24", "1:24", "")
.build();
insta::assert_snapshot!(env.render(&diagnostic), @r#"
error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
--> example.py:1:25
|
1 | nested_fstrings = f'␈{f'{f'␛'}'}'
| ^
|
"#);
}
#[test]
fn multiple_unprintable_characters() -> std::io::Result<()> {
let mut env = TestEnvironment::new();
env.add("example.py", "");
env.format(DiagnosticFormat::Full);
let diagnostic = env
.builder(
"invalid-character-sub",
Severity::Error,
r#"Invalid unescaped character SUB, use "\x1A" instead"#,
)
.primary("example.py", "1:1", "1:1", "")
.build();
insta::assert_snapshot!(env.render(&diagnostic), @r#"
error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
--> example.py:1:2
|
1 | ␈
| ^
|
"#);
Ok(())
}
}

View File

@@ -20,6 +20,7 @@ ty_python_semantic = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true, optional = true }
memchr = { workspace = true }
salsa = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }

View File

@@ -1,3 +1,4 @@
use crate::StringImports;
use ruff_python_ast::visitor::source_order::{
SourceOrderVisitor, walk_expr, walk_module, walk_stmt,
};
@@ -10,13 +11,13 @@ pub(crate) struct Collector<'a> {
/// The path to the current module.
module_path: Option<&'a [String]>,
/// Whether to detect imports from string literals.
string_imports: bool,
string_imports: StringImports,
/// The collected imports from the Python AST.
imports: Vec<CollectedImport>,
}
impl<'a> Collector<'a> {
pub(crate) fn new(module_path: Option<&'a [String]>, string_imports: bool) -> Self {
pub(crate) fn new(module_path: Option<&'a [String]>, string_imports: StringImports) -> Self {
Self {
module_path,
string_imports,
@@ -118,7 +119,7 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
| Stmt::Continue(_)
| Stmt::IpyEscapeCommand(_) => {
// Only traverse simple statements when string imports is enabled.
if self.string_imports {
if self.string_imports.enabled {
walk_stmt(self, stmt);
}
}
@@ -126,20 +127,26 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
}
fn visit_expr(&mut self, expr: &'ast Expr) {
if self.string_imports {
if self.string_imports.enabled {
if let Expr::StringLiteral(ast::ExprStringLiteral {
value,
range: _,
node_index: _,
}) = expr
{
// Determine whether the string literal "looks like" an import statement: contains
// a dot, and consists solely of valid Python identifiers.
let value = value.to_str();
if let Some(module_name) = ModuleName::new(value) {
self.imports.push(CollectedImport::Import(module_name));
// Determine whether the string literal "looks like" an import statement: contains
// the requisite number of dots, and consists solely of valid Python identifiers.
if self.string_imports.min_dots == 0
|| memchr::memchr_iter(b'.', value.as_bytes()).count()
>= self.string_imports.min_dots
{
if let Some(module_name) = ModuleName::new(value) {
self.imports.push(CollectedImport::Import(module_name));
}
}
}
walk_expr(self, expr);
}
}

View File

@@ -9,7 +9,7 @@ use ruff_python_parser::{Mode, ParseOptions, parse};
use crate::collector::Collector;
pub use crate::db::ModuleDb;
use crate::resolver::Resolver;
pub use crate::settings::{AnalyzeSettings, Direction};
pub use crate::settings::{AnalyzeSettings, Direction, StringImports};
mod collector;
mod db;
@@ -26,7 +26,7 @@ impl ModuleImports {
db: &ModuleDb,
path: &SystemPath,
package: Option<&SystemPath>,
string_imports: bool,
string_imports: StringImports,
) -> Result<Self> {
// Read and parse the source code.
let source = std::fs::read_to_string(path)?;
@@ -42,13 +42,11 @@ impl ModuleImports {
// Resolve the imports.
let mut resolved_imports = ModuleImports::default();
for import in imports {
let Some(resolved) = Resolver::new(db).resolve(import) else {
continue;
};
let Some(path) = resolved.as_system_path() else {
continue;
};
resolved_imports.insert(path.to_path_buf());
for resolved in Resolver::new(db).resolve(import) {
if let Some(path) = resolved.as_system_path() {
resolved_imports.insert(path.to_path_buf());
}
}
}
Ok(resolved_imports)

View File

@@ -1,5 +1,5 @@
use ruff_db::files::FilePath;
use ty_python_semantic::resolve_module;
use ty_python_semantic::{ModuleName, resolve_module, resolve_real_module};
use crate::ModuleDb;
use crate::collector::CollectedImport;
@@ -16,24 +16,67 @@ impl<'a> Resolver<'a> {
}
/// Resolve the [`CollectedImport`] into a [`FilePath`].
pub(crate) fn resolve(&self, import: CollectedImport) -> Option<&'a FilePath> {
pub(crate) fn resolve(&self, import: CollectedImport) -> impl Iterator<Item = &'a FilePath> {
match import {
CollectedImport::Import(import) => {
let module = resolve_module(self.db, &import)?;
Some(module.file(self.db)?.path(self.db))
// Attempt to resolve the module (e.g., given `import foo`, look for `foo`).
let file = self.resolve_module(&import);
// If the file is a stub, look for the corresponding source file.
let source_file = file
.is_some_and(|file| file.extension() == Some("pyi"))
.then(|| self.resolve_real_module(&import))
.flatten();
std::iter::once(file)
.chain(std::iter::once(source_file))
.flatten()
}
CollectedImport::ImportFrom(import) => {
// Attempt to resolve the member (e.g., given `from foo import bar`, look for `foo.bar`).
if let Some(file) = self.resolve_module(&import) {
// If the file is a stub, look for the corresponding source file.
let source_file = (file.extension() == Some("pyi"))
.then(|| self.resolve_real_module(&import))
.flatten();
return std::iter::once(Some(file))
.chain(std::iter::once(source_file))
.flatten();
}
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
let parent = import.parent();
let file = parent
.as_ref()
.and_then(|parent| self.resolve_module(parent));
let module = resolve_module(self.db, &import).or_else(|| {
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
// If the file is a stub, look for the corresponding source file.
let source_file = file
.is_some_and(|file| file.extension() == Some("pyi"))
.then(|| {
parent
.as_ref()
.and_then(|parent| self.resolve_real_module(parent))
})
.flatten();
resolve_module(self.db, &parent?)
})?;
Some(module.file(self.db)?.path(self.db))
std::iter::once(file)
.chain(std::iter::once(source_file))
.flatten()
}
}
}
/// Resolves a module name to a module.
fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_module(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}
/// Resolves a module name to a module (stubs not allowed).
fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_real_module(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}
}

View File

@@ -11,7 +11,7 @@ pub struct AnalyzeSettings {
pub exclude: FilePatternSet,
pub preview: PreviewMode,
pub target_version: PythonVersion,
pub detect_string_imports: bool,
pub string_imports: StringImports,
pub include_dependencies: BTreeMap<PathBuf, (PathBuf, Vec<String>)>,
pub extension: ExtensionMapping,
}
@@ -26,7 +26,7 @@ impl fmt::Display for AnalyzeSettings {
self.exclude,
self.preview,
self.target_version,
self.detect_string_imports,
self.string_imports,
self.extension | debug,
self.include_dependencies | debug,
]
@@ -35,6 +35,31 @@ impl fmt::Display for AnalyzeSettings {
}
}
#[derive(Debug, Copy, Clone, CacheKey)]
pub struct StringImports {
pub enabled: bool,
pub min_dots: usize,
}
impl Default for StringImports {
fn default() -> Self {
Self {
enabled: false,
min_dots: 2,
}
}
}
impl fmt::Display for StringImports {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.enabled {
write!(f, "enabled (min_dots: {})", self.min_dots)
} else {
write!(f, "disabled")
}
}
}
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, CacheKey)]
#[cfg_attr(
feature = "serde",

View File

@@ -25,5 +25,5 @@ def my_func():
# t-strings - all ok
t"0.0.0.0"
"0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
"0.0.0.0" f"0.0.0.0{expr}0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
t"0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
t"0.0.0.0" t"0.0.0.0{expr}0.0.0.0" t"0.0.0.0{expr}0.0.0.0"

View File

@@ -94,7 +94,7 @@ except Exception:
logging.error("...", exc_info=True)
from logging import error, exception
from logging import critical, error, exception
try:
pass
@@ -114,6 +114,23 @@ except Exception:
error("...", exc_info=None)
try:
pass
except Exception:
critical("...")
try:
pass
except Exception:
critical("...", exc_info=False)
try:
pass
except Exception:
critical("...", exc_info=None)
try:
pass
except Exception:
@@ -125,6 +142,13 @@ try:
except Exception:
error("...", exc_info=True)
try:
pass
except Exception:
critical("...", exc_info=True)
try:
...
except Exception as e:

View File

@@ -650,3 +650,17 @@ f"""This is a test. {
if True else
"Don't add a trailing comma here ->"
}"""
type X[
T
] = T
def f[
T
](): pass
class C[
T
]: pass
type X[T,] = T
def f[T,](): pass
class C[T,]: pass

View File

@@ -289,4 +289,19 @@ def f():
i = "xyz"
result = []
for i in range(3):
result.append((x for x in [i]))
result.append((x for x in [i]))
G_INDEX = None
def f():
global G_INDEX
result = []
for G_INDEX in range(3):
result.append(G_INDEX)
def f():
NL_INDEX = None
def x():
nonlocal NL_INDEX
result = []
for NL_INDEX in range(3):
result.append(NL_INDEX)

View File

@@ -143,3 +143,23 @@ class NotAMethodButHardToDetect:
# without risking false positives elsewhere or introducing complex heuristics
# that users would find surprising and confusing
FOO = sorted([x for x in BAR], key=lambda x: x.baz)
# https://github.com/astral-sh/ruff/issues/19305
import pytest
@pytest.fixture
def my_fixture_with_param(request):
return request.param
@pytest.fixture()
def my_fixture_with_param2(request):
return request.param
# Decorated function (should be ignored)
def custom_decorator(func):
return func
@custom_decorator
def add(x, y):
return x + y

View File

@@ -55,3 +55,12 @@ _ = Decimal(0.1)
_ = Decimal(-0.5)
_ = Decimal(5.0)
_ = decimal.Decimal(4.2)
# Cases with int and bool - should produce safe fixes
_ = Decimal.from_float(1)
_ = Decimal.from_float(True)
# Cases with non-finite floats - should produce safe fixes
_ = Decimal.from_float(float("-nan"))
_ = Decimal.from_float(float("\x2dnan"))
_ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))

View File

@@ -65,3 +65,62 @@ class Foo:
bar = "should've used attrs"
def __post_init__(self, bar: str = "ahhh", baz: str = "hmm") -> None: ...
# https://github.com/astral-sh/ruff/issues/18950
@dataclass
class Foo:
def __post_init__(self, bar: int = (x := 1)) -> None:
pass
@dataclass
class Foo:
def __post_init__(
self,
bar: int = (x := 1) # comment
,
baz: int = (y := 2), # comment
foo = (a := 1) # comment
,
faz = (b := 2), # comment
) -> None:
pass
@dataclass
class Foo:
def __post_init__(
self,
bar: int = 1, # comment
baz: int = 2, # comment
) -> None:
pass
@dataclass
class Foo:
def __post_init__(
self,
arg1: int = (1) # comment
,
arg2: int = ((1)) # comment
,
arg2: int = (i for i in range(10)) # comment
,
) -> None:
pass
# makes little sense, but is valid syntax
def fun_with_python_syntax():
@dataclass
class Foo:
def __post_init__(
self,
bar: (int) = (yield from range(5)) # comment
,
) -> None:
...
return Foo

View File

@@ -53,3 +53,16 @@ regex.subn(br"""eak your machine with rm -""", rf"""/""")
regex.splititer(both, non_literal)
regex.subf(f, lambda _: r'means', '"format"')
regex.subfn(fn, f'''a$1n't''', lambda: "'function'")
# https://github.com/astral-sh/ruff/issues/16713
re.compile("\a\f\n\r\t\u27F2\U0001F0A1\v\x41") # with unsafe fix
re.compile("\b") # without fix
re.compile("\"") # without fix
re.compile("\'") # without fix
re.compile('\"') # without fix
re.compile('\'') # without fix
re.compile("\\") # without fix
re.compile("\101") # without fix
re.compile("a\
b") # without fix

View File

@@ -91,3 +91,20 @@ regex.subf(
br''br""br''
)
regex.subfn(br'I\s\nee*d\s[O0o]me\x20\Qoffe\E, ' br'b')
# https://github.com/astral-sh/ruff/issues/16713
re.compile(
"["
"\U0001F600-\U0001F64F" # emoticons
"\U0001F300-\U0001F5FF" # symbols & pictographs
"\U0001F680-\U0001F6FF" # transport & map symbols
"\U0001F1E0-\U0001F1FF" # flags (iOS)
"\U00002702-\U000027B0"
"\U000024C2-\U0001F251"
"\u200d" # zero width joiner
"\u200c" # zero width non-joiner
"\\u200c" # must not be escaped in a raw string
"]+",
flags=re.UNICODE,
)

View File

@@ -0,0 +1,3 @@
import re
re.compile("\N{Partial Differential}") # with unsafe fix if python target is 3.8 or higher, else without fix

View File

@@ -3216,6 +3216,11 @@ impl<'a> LintContext<'a> {
pub(crate) fn iter(&mut self) -> impl Iterator<Item = &Diagnostic> {
self.diagnostics.get_mut().iter()
}
/// The [`LinterSettings`] for the current analysis, including the enabled rules.
pub(crate) const fn settings(&self) -> &LinterSettings {
self.settings
}
}
/// An abstraction for mutating a diagnostic.

View File

@@ -16,7 +16,6 @@ use crate::rules::{
eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
flake8_pyi, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff,
};
use crate::settings::LinterSettings;
use super::ast::LintContext;
@@ -27,7 +26,6 @@ pub(crate) fn check_tokens(
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
settings: &LinterSettings,
source_type: PySourceType,
cell_offsets: Option<&CellOffsets>,
context: &mut LintContext,
@@ -42,15 +40,8 @@ pub(crate) fn check_tokens(
Rule::BlankLinesAfterFunctionOrClass,
Rule::BlankLinesBeforeNestedDefinition,
]) {
BlankLinesChecker::new(
locator,
stylist,
settings,
source_type,
cell_offsets,
context,
)
.check_lines(tokens);
BlankLinesChecker::new(locator, stylist, source_type, cell_offsets, context)
.check_lines(tokens);
}
if context.is_rule_enabled(Rule::BlanketTypeIgnore) {
@@ -63,12 +54,12 @@ pub(crate) fn check_tokens(
if context.is_rule_enabled(Rule::AmbiguousUnicodeCharacterComment) {
for range in comment_ranges {
ruff::rules::ambiguous_unicode_character_comment(context, locator, range, settings);
ruff::rules::ambiguous_unicode_character_comment(context, locator, range);
}
}
if context.is_rule_enabled(Rule::CommentedOutCode) {
eradicate::rules::commented_out_code(context, locator, comment_ranges, settings);
eradicate::rules::commented_out_code(context, locator, comment_ranges);
}
if context.is_rule_enabled(Rule::UTF8EncodingDeclaration) {
@@ -110,7 +101,7 @@ pub(crate) fn check_tokens(
Rule::SingleLineImplicitStringConcatenation,
Rule::MultiLineImplicitStringConcatenation,
]) {
flake8_implicit_str_concat::rules::implicit(context, tokens, locator, indexer, settings);
flake8_implicit_str_concat::rules::implicit(context, tokens, locator, indexer);
}
if context.any_rule_enabled(&[

View File

@@ -188,7 +188,6 @@ pub fn check_path(
locator,
indexer,
stylist,
settings,
source_type,
source_kind.as_ipy_notebook().map(Notebook::cell_offsets),
&mut context,
@@ -473,7 +472,7 @@ pub fn lint_only(
&& !is_py314_support_enabled(settings)
{
warn_user_once!(
"Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning."
"Support for Python 3.14 is in preview and may undergo breaking changes. Enable `preview` to remove this warning."
);
}
@@ -584,7 +583,7 @@ pub fn lint_fix<'a>(
&& !is_py314_support_enabled(settings)
{
warn_user_once!(
"Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning."
"Support for Python 3.14 is in preview and may undergo breaking changes. Enable `preview` to remove this warning."
);
}

View File

@@ -118,86 +118,6 @@ impl<'a> Locator<'a> {
}
}
/// Finds the closest [`TextSize`] not less than the offset given for which
/// `is_char_boundary` is `true`. Unless the offset given is greater than
/// the length of the underlying contents, in which case, the length of the
/// contents is returned.
///
/// Can be replaced with `str::ceil_char_boundary` once it's stable.
///
/// # Examples
///
/// From `std`:
///
/// ```
/// use ruff_text_size::{Ranged, TextSize};
/// use ruff_linter::Locator;
///
/// let locator = Locator::new("❤️🧡💛💚💙💜");
/// assert_eq!(locator.text_len(), TextSize::from(26));
/// assert!(!locator.contents().is_char_boundary(13));
///
/// let closest = locator.ceil_char_boundary(TextSize::from(13));
/// assert_eq!(closest, TextSize::from(14));
/// assert_eq!(&locator.contents()[..closest.to_usize()], "❤️🧡💛");
/// ```
///
/// Additional examples:
///
/// ```
/// use ruff_text_size::{Ranged, TextRange, TextSize};
/// use ruff_linter::Locator;
///
/// let locator = Locator::new("Hello");
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(5)),
/// TextSize::from(5)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(6)),
/// TextSize::from(5)
/// );
///
/// let locator = Locator::new("α");
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(1)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(2)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(3)),
/// TextSize::from(2)
/// );
/// ```
pub fn ceil_char_boundary(&self, offset: TextSize) -> TextSize {
let upper_bound = offset
.to_u32()
.saturating_add(4)
.min(self.text_len().to_u32());
(offset.to_u32()..upper_bound)
.map(TextSize::from)
.find(|offset| self.contents.is_char_boundary(offset.to_usize()))
.unwrap_or_else(|| TextSize::from(upper_bound))
}
/// Take the source code between the given [`TextRange`].
#[inline]
pub fn slice<T: Ranged>(&self, ranged: T) -> &'a str {

View File

@@ -6,12 +6,13 @@ use bitflags::bitflags;
use colored::Colorize;
use ruff_annotate_snippets::{Level, Renderer, Snippet};
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, SecondaryCode};
use ruff_db::diagnostic::{
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, SecondaryCode, ceil_char_boundary,
};
use ruff_notebook::NotebookIndex;
use ruff_source_file::OneIndexed;
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::Locator;
use crate::line_width::{IndentWidth, LineWidthBuilder};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext};
@@ -76,6 +77,12 @@ impl TextEmitter {
self.config = self.config.preview(preview);
self
}
#[must_use]
pub fn with_color(mut self, color: bool) -> Self {
self.config = self.config.color(color);
self
}
}
impl Emitter for TextEmitter {
@@ -370,9 +377,8 @@ impl<'a> SourceCode<'a> {
if self.text.as_bytes()[self.annotation_range.start().to_usize() - 1] != b'\n' {
return self;
}
let locator = Locator::new(&self.text);
let start = self.annotation_range.start();
let end = locator.ceil_char_boundary(start + TextSize::from(1));
let end = ceil_char_boundary(&self.text, start + TextSize::from(1));
SourceCode {
annotation_range: TextRange::new(start, end),
..self

View File

@@ -225,3 +225,8 @@ pub(crate) const fn is_assert_raises_exception_call_enabled(settings: &LinterSet
pub(crate) const fn is_add_future_annotations_imports_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19390
pub(crate) const fn is_trailing_comma_type_params_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -5,7 +5,6 @@ use ruff_text_size::TextRange;
use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::settings::LinterSettings;
use crate::{Edit, Fix, FixAvailability, Violation};
use crate::rules::eradicate::detection::comment_contains_code;
@@ -51,7 +50,6 @@ pub(crate) fn commented_out_code(
context: &LintContext,
locator: &Locator,
comment_ranges: &CommentRanges,
settings: &LinterSettings,
) {
let mut comments = comment_ranges.into_iter().peekable();
// Iterate over all comments in the document.
@@ -65,7 +63,9 @@ pub(crate) fn commented_out_code(
}
// Verify that the comment is on its own line, and that it contains code.
if is_own_line_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
if is_own_line_comment(line)
&& comment_contains_code(line, &context.settings().task_tags[..])
{
if let Some(mut diagnostic) =
context.report_diagnostic_if_enabled(CommentedOutCode, range)
{

View File

@@ -47,9 +47,10 @@ use crate::checkers::ast::Checker;
/// raise
/// ```
///
/// Exceptions that are logged via `logging.exception()` or `logging.error()`
/// with `exc_info` enabled will _not_ be flagged, as this is a common pattern
/// for propagating exception traces:
/// Exceptions that are logged via `logging.exception()` or are logged via
/// `logging.error()` or `logging.critical()` with `exc_info` enabled will
/// _not_ be flagged, as this is a common pattern for propagating exception
/// traces:
/// ```python
/// try:
/// foo()
@@ -201,7 +202,7 @@ impl<'a> StatementVisitor<'a> for LogExceptionVisitor<'a> {
) {
if match attr.as_str() {
"exception" => true,
"error" => arguments
"error" | "critical" => arguments
.find_keyword("exc_info")
.is_some_and(|keyword| is_const_true(&keyword.value)),
_ => false,
@@ -214,7 +215,7 @@ impl<'a> StatementVisitor<'a> for LogExceptionVisitor<'a> {
if self.semantic.resolve_qualified_name(func).is_some_and(
|qualified_name| match qualified_name.segments() {
["logging", "exception"] => true,
["logging", "error"] => arguments
["logging", "error" | "critical"] => arguments
.find_keyword("exc_info")
.is_some_and(|keyword| is_const_true(&keyword.value)),
_ => false,

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_blind_except/mod.rs
snapshot_kind: text
---
BLE.py:25:8: BLE001 Do not catch blind exception: `BaseException`
|
@@ -121,3 +120,30 @@ BLE.py:113:8: BLE001 Do not catch blind exception: `Exception`
| ^^^^^^^^^ BLE001
114 | error("...", exc_info=None)
|
BLE.py:119:8: BLE001 Do not catch blind exception: `Exception`
|
117 | try:
118 | pass
119 | except Exception:
| ^^^^^^^^^ BLE001
120 | critical("...")
|
BLE.py:125:8: BLE001 Do not catch blind exception: `Exception`
|
123 | try:
124 | pass
125 | except Exception:
| ^^^^^^^^^ BLE001
126 | critical("...", exc_info=False)
|
BLE.py:131:8: BLE001 Do not catch blind exception: `Exception`
|
129 | try:
130 | pass
131 | except Exception:
| ^^^^^^^^^ BLE001
132 | critical("...", exc_info=None)
|

View File

@@ -27,4 +27,23 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("COM81.py"))]
#[test_case(Path::new("COM81_syntax_error.py"))]
fn preview_rules(path: &Path) -> Result<()> {
let snapshot = format!("preview__{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_commas").join(path).as_path(),
&settings::LinterSettings {
preview: crate::settings::types::PreviewMode::Enabled,
..settings::LinterSettings::for_rules(vec![
Rule::MissingTrailingComma,
Rule::TrailingCommaOnBareTuple,
Rule::ProhibitedTrailingComma,
])
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -5,6 +5,8 @@ use ruff_text_size::{Ranged, TextRange};
use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::preview::is_trailing_comma_type_params_enabled;
use crate::settings::LinterSettings;
use crate::{AlwaysFixableViolation, Violation};
use crate::{Edit, Fix};
@@ -24,6 +26,8 @@ enum TokenType {
Def,
For,
Lambda,
Class,
Type,
Irrelevant,
}
@@ -69,6 +73,8 @@ impl From<(TokenKind, TextRange)> for SimpleToken {
TokenKind::Lbrace => TokenType::OpeningCurlyBracket,
TokenKind::Rbrace => TokenType::ClosingBracket,
TokenKind::Def => TokenType::Def,
TokenKind::Class => TokenType::Class,
TokenKind::Type => TokenType::Type,
TokenKind::For => TokenType::For,
TokenKind::Lambda => TokenType::Lambda,
// Import treated like a function.
@@ -98,6 +104,8 @@ enum ContextType {
Dict,
/// Lambda parameter list, e.g. `lambda a, b`.
LambdaParameters,
/// Type parameter list, e.g. `def foo[T, U](): ...`
TypeParameters,
}
/// Comma context - described a comma-delimited "situation".
@@ -290,7 +298,7 @@ pub(crate) fn trailing_commas(
}
// Update the comma context stack.
let context = update_context(token, prev, prev_prev, &mut stack);
let context = update_context(token, prev, prev_prev, &mut stack, lint_context.settings());
check_token(token, prev, prev_prev, context, locator, lint_context);
@@ -326,6 +334,7 @@ fn check_token(
ContextType::No => false,
ContextType::FunctionParameters => true,
ContextType::CallArguments => true,
ContextType::TypeParameters => true,
// `(1)` is not equivalent to `(1,)`.
ContextType::Tuple => context.num_commas != 0,
// `x[1]` is not equivalent to `x[1,]`.
@@ -408,6 +417,7 @@ fn update_context(
prev: SimpleToken,
prev_prev: SimpleToken,
stack: &mut Vec<Context>,
settings: &LinterSettings,
) -> Context {
let new_context = match token.ty {
TokenType::OpeningBracket => match (prev.ty, prev_prev.ty) {
@@ -417,6 +427,17 @@ fn update_context(
}
_ => Context::new(ContextType::Tuple),
},
TokenType::OpeningSquareBracket if is_trailing_comma_type_params_enabled(settings) => {
match (prev.ty, prev_prev.ty) {
(TokenType::Named, TokenType::Def | TokenType::Class | TokenType::Type) => {
Context::new(ContextType::TypeParameters)
}
(TokenType::ClosingBracket | TokenType::Named | TokenType::String, _) => {
Context::new(ContextType::Subscript)
}
_ => Context::new(ContextType::List),
}
}
TokenType::OpeningSquareBracket => match prev.ty {
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
Context::new(ContextType::Subscript)

View File

@@ -0,0 +1,30 @@
---
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
---
COM81_syntax_error.py:3:5: SyntaxError: Starred expression cannot be used here
|
1 | # Check for `flake8-commas` violation for a file containing syntax errors.
2 | (
3 | *args
| ^^^^^
4 | )
|
COM81_syntax_error.py:6:9: SyntaxError: Type parameter list cannot be empty
|
4 | )
5 |
6 | def foo[(param1='test', param2='test',):
| ^
7 | pass
|
COM81_syntax_error.py:6:38: COM819 Trailing comma prohibited
|
4 | )
5 |
6 | def foo[(param1='test', param2='test',):
| ^ COM819
7 | pass
|
= help: Remove trailing comma

View File

@@ -11,7 +11,6 @@ use ruff_text_size::{Ranged, TextRange};
use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::settings::LinterSettings;
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
@@ -108,13 +107,15 @@ pub(crate) fn implicit(
tokens: &Tokens,
locator: &Locator,
indexer: &Indexer,
settings: &LinterSettings,
) {
for (a_token, b_token) in tokens
.iter()
.filter(|token| {
token.kind() != TokenKind::Comment
&& (settings.flake8_implicit_str_concat.allow_multiline
&& (context
.settings()
.flake8_implicit_str_concat
.allow_multiline
|| token.kind() != TokenKind::NonLogicalNewline)
})
.tuple_windows()

View File

@@ -249,6 +249,11 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF
.iter()
.find(|binding| for_stmt.target.range() == binding.range)
.unwrap();
// If the target variable is global (e.g., `global INDEX`) or nonlocal (e.g., `nonlocal INDEX`),
// then it is intended to be used elsewhere outside the for loop.
if target_binding.is_global() || target_binding.is_nonlocal() {
return;
}
// If any references to the loop target variable are after the loop,
// then converting it into a comprehension would cause a NameError
if target_binding

View File

@@ -263,5 +263,7 @@ PERF401.py:292:9: PERF401 Use a list comprehension to create a transformed list
291 | for i in range(3):
292 | result.append((x for x in [i]))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF401
293 |
294 | G_INDEX = None
|
= help: Replace for loop with list comprehension

View File

@@ -612,6 +612,8 @@ PERF401.py:292:9: PERF401 [*] Use a list comprehension to create a transformed l
291 | for i in range(3):
292 | result.append((x for x in [i]))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF401
293 |
294 | G_INDEX = None
|
= help: Replace for loop with list comprehension
@@ -623,3 +625,6 @@ PERF401.py:292:9: PERF401 [*] Use a list comprehension to create a transformed l
291 |- for i in range(3):
292 |- result.append((x for x in [i]))
290 |+ result = [(x for x in [i]) for i in range(3)]
293 291 |
294 292 | G_INDEX = None
295 293 | def f():

View File

@@ -21,7 +21,6 @@ use crate::checkers::ast::{DiagnosticGuard, LintContext};
use crate::checkers::logical_lines::expand_indent;
use crate::line_width::IndentWidth;
use crate::rules::pycodestyle::helpers::is_non_logical_token;
use crate::settings::LinterSettings;
use crate::{AlwaysFixableViolation, Edit, Fix, Locator, Violation};
/// Number of blank lines around top level classes and functions.
@@ -694,14 +693,12 @@ pub(crate) struct BlankLinesChecker<'a, 'b> {
source_type: PySourceType,
cell_offsets: Option<&'a CellOffsets>,
context: &'a LintContext<'b>,
settings: &'a LinterSettings,
}
impl<'a, 'b> BlankLinesChecker<'a, 'b> {
pub(crate) fn new(
locator: &'a Locator<'a>,
stylist: &'a Stylist<'a>,
settings: &'a LinterSettings,
source_type: PySourceType,
cell_offsets: Option<&'a CellOffsets>,
context: &'a LintContext<'b>,
@@ -712,7 +709,6 @@ impl<'a, 'b> BlankLinesChecker<'a, 'b> {
source_type,
cell_offsets,
context,
settings,
}
}
@@ -733,7 +729,7 @@ impl<'a, 'b> BlankLinesChecker<'a, 'b> {
let line_preprocessor = LinePreprocessor::new(
tokens,
self.locator,
self.settings.tab_size,
self.context.settings().tab_size,
self.cell_offsets,
);
@@ -879,7 +875,8 @@ impl<'a, 'b> BlankLinesChecker<'a, 'b> {
// `isort` defaults to 2 if before a class or function definition (except in stubs where it is one) and 1 otherwise.
// Defaulting to 2 (or 1 in stubs) here is correct because the variable is only used when testing the
// blank lines before a class or function definition.
u32::try_from(self.settings.isort.lines_after_imports).unwrap_or(max_lines_level)
u32::try_from(self.context.settings().isort.lines_after_imports)
.unwrap_or(max_lines_level)
} else {
max_lines_level
}
@@ -941,8 +938,10 @@ impl<'a, 'b> BlankLinesChecker<'a, 'b> {
(LogicalLineKind::Import, Follows::FromImport)
| (LogicalLineKind::FromImport, Follows::Import)
) {
max_lines_level
.max(u32::try_from(self.settings.isort.lines_between_types).unwrap_or(u32::MAX))
max_lines_level.max(
u32::try_from(self.context.settings().isort.lines_between_types)
.unwrap_or(u32::MAX),
)
} else {
expected_blank_lines_before_definition
};

View File

@@ -6,8 +6,9 @@ use ruff_python_ast::{
use ruff_python_semantic::{SemanticModel, analyze::typing};
use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::fix;
use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
/// ## What it does
/// Checks for access to the first or last element of `str.split()` or `str.rsplit()` without
@@ -35,10 +36,14 @@ use crate::checkers::ast::Checker;
/// url = "www.example.com"
/// suffix = url.rsplit(".", maxsplit=1)[-1]
/// ```
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe for `split()`/`rsplit()` calls that contain `**kwargs`, as
/// adding a `maxsplit` keyword to such a call may lead to a duplicate keyword argument error.
#[derive(ViolationMetadata)]
pub(crate) struct MissingMaxsplitArg {
index: SliceBoundary,
actual_split_type: String,
suggested_split_type: String,
}
/// Represents the index of the slice used for this rule (which can only be 0 or -1)
@@ -47,25 +52,27 @@ enum SliceBoundary {
Last,
}
impl Violation for MissingMaxsplitArg {
impl AlwaysFixableViolation for MissingMaxsplitArg {
#[derive_message_formats]
fn message(&self) -> String {
let MissingMaxsplitArg {
index,
actual_split_type,
actual_split_type: _,
suggested_split_type,
} = self;
let suggested_split_type = match index {
SliceBoundary::First => "split",
SliceBoundary::Last => "rsplit",
};
format!("Replace with `{suggested_split_type}(..., maxsplit=1)`.")
}
fn fix_title(&self) -> String {
let MissingMaxsplitArg {
actual_split_type,
suggested_split_type,
} = self;
if actual_split_type == suggested_split_type {
format!("Pass `maxsplit=1` into `str.{actual_split_type}()`")
} else {
format!(
"Instead of `str.{actual_split_type}()`, call `str.{suggested_split_type}()` and pass `maxsplit=1`",
)
format!("Use `str.{suggested_split_type}()` and pass `maxsplit=1`")
}
}
}
@@ -123,8 +130,8 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
};
// Check the function is "split" or "rsplit"
let attr = attr.as_str();
if !matches!(attr, "split" | "rsplit") {
let actual_split_type = attr.as_str();
if !matches!(actual_split_type, "split" | "rsplit") {
return;
}
@@ -161,11 +168,48 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
}
}
checker.report_diagnostic(
let suggested_split_type = match slice_boundary {
SliceBoundary::First => "split",
SliceBoundary::Last => "rsplit",
};
let maxsplit_argument_edit = fix::edits::add_argument(
"maxsplit=1",
arguments,
checker.comment_ranges(),
checker.locator().contents(),
);
// Only change `actual_split_type` if it doesn't match `suggested_split_type`
let split_type_edit: Option<Edit> = if actual_split_type == suggested_split_type {
None
} else {
Some(Edit::range_replacement(
suggested_split_type.to_string(),
attr.range(),
))
};
let mut diagnostic = checker.report_diagnostic(
MissingMaxsplitArg {
index: slice_boundary,
actual_split_type: attr.to_string(),
actual_split_type: actual_split_type.to_string(),
suggested_split_type: suggested_split_type.to_string(),
},
expr.range(),
);
diagnostic.set_fix(Fix::applicable_edits(
maxsplit_argument_edit,
split_type_edit,
// If keyword.arg is `None` (i.e. if the function call contains `**kwargs`), mark the fix as unsafe
if arguments
.keywords
.iter()
.any(|keyword| keyword.arg.is_none())
{
Applicability::Unsafe
} else {
Applicability::Safe
},
));
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
missing_maxsplit_arg.py:14:1: PLC0207 Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:14:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
12 | # Errors
13 | ## Test split called directly on string literal
@@ -10,8 +10,19 @@ missing_maxsplit_arg.py:14:1: PLC0207 Pass `maxsplit=1` into `str.split()`
15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:15:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
Safe fix
11 11 |
12 12 | # Errors
13 13 | ## Test split called directly on string literal
14 |-"1,2,3".split(",")[0] # [missing-maxsplit-arg]
14 |+"1,2,3".split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
15 15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
16 16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
17 17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:15:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
13 | ## Test split called directly on string literal
14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
@@ -20,8 +31,19 @@ missing_maxsplit_arg.py:15:1: PLC0207 Instead of `str.split()`, call `str.rsplit
16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
missing_maxsplit_arg.py:16:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
Safe fix
12 12 | # Errors
13 13 | ## Test split called directly on string literal
14 14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
15 |-"1,2,3".split(",")[-1] # [missing-maxsplit-arg]
15 |+"1,2,3".rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
16 16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
17 17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
18 18 |
missing_maxsplit_arg.py:16:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
@@ -29,8 +51,19 @@ missing_maxsplit_arg.py:16:1: PLC0207 Instead of `str.rsplit()`, call `str.split
| ^^^^^^^^^^^^^^^^^^^^^^ PLC0207
17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
missing_maxsplit_arg.py:17:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
Safe fix
13 13 | ## Test split called directly on string literal
14 14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
15 15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
16 |-"1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
16 |+"1,2,3".split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
17 17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
18 18 |
19 19 | ## Test split called on string variable
missing_maxsplit_arg.py:17:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
@@ -39,8 +72,19 @@ missing_maxsplit_arg.py:17:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
18 |
19 | ## Test split called on string variable
|
= help: Pass `maxsplit=1` into `str.rsplit()`
missing_maxsplit_arg.py:20:1: PLC0207 Pass `maxsplit=1` into `str.split()`
Safe fix
14 14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
15 15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
16 16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
17 |-"1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
17 |+"1,2,3".rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
18 18 |
19 19 | ## Test split called on string variable
20 20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:20:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
19 | ## Test split called on string variable
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
@@ -48,8 +92,19 @@ missing_maxsplit_arg.py:20:1: PLC0207 Pass `maxsplit=1` into `str.split()`
21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:21:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
Safe fix
17 17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
18 18 |
19 19 | ## Test split called on string variable
20 |-SEQ.split(",")[0] # [missing-maxsplit-arg]
20 |+SEQ.split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
21 21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
22 22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
23 23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:21:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
19 | ## Test split called on string variable
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
@@ -58,8 +113,19 @@ missing_maxsplit_arg.py:21:1: PLC0207 Instead of `str.split()`, call `str.rsplit
22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
missing_maxsplit_arg.py:22:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
Safe fix
18 18 |
19 19 | ## Test split called on string variable
20 20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
21 |-SEQ.split(",")[-1] # [missing-maxsplit-arg]
21 |+SEQ.rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
22 22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
23 23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
24 24 |
missing_maxsplit_arg.py:22:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
@@ -67,8 +133,19 @@ missing_maxsplit_arg.py:22:1: PLC0207 Instead of `str.rsplit()`, call `str.split
| ^^^^^^^^^^^^^^^^^^ PLC0207
23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
missing_maxsplit_arg.py:23:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
Safe fix
19 19 | ## Test split called on string variable
20 20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
21 21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
22 |-SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
22 |+SEQ.split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
23 23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
24 24 |
25 25 | ## Test split called on class attribute
missing_maxsplit_arg.py:23:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
@@ -77,8 +154,19 @@ missing_maxsplit_arg.py:23:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
24 |
25 | ## Test split called on class attribute
|
= help: Pass `maxsplit=1` into `str.rsplit()`
missing_maxsplit_arg.py:26:1: PLC0207 Pass `maxsplit=1` into `str.split()`
Safe fix
20 20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
21 21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
22 22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
23 |-SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
23 |+SEQ.rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
24 24 |
25 25 | ## Test split called on class attribute
26 26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:26:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
25 | ## Test split called on class attribute
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
@@ -86,8 +174,19 @@ missing_maxsplit_arg.py:26:1: PLC0207 Pass `maxsplit=1` into `str.split()`
27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:27:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
Safe fix
23 23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
24 24 |
25 25 | ## Test split called on class attribute
26 |-Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
26 |+Foo.class_str.split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
27 27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
28 28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
29 29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:27:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
25 | ## Test split called on class attribute
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
@@ -96,8 +195,19 @@ missing_maxsplit_arg.py:27:1: PLC0207 Instead of `str.split()`, call `str.rsplit
28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
missing_maxsplit_arg.py:28:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
Safe fix
24 24 |
25 25 | ## Test split called on class attribute
26 26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
27 |-Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
27 |+Foo.class_str.rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
28 28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
29 29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
30 30 |
missing_maxsplit_arg.py:28:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
@@ -105,8 +215,19 @@ missing_maxsplit_arg.py:28:1: PLC0207 Instead of `str.rsplit()`, call `str.split
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
missing_maxsplit_arg.py:29:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
Safe fix
25 25 | ## Test split called on class attribute
26 26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
27 27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
28 |-Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
28 |+Foo.class_str.split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
29 29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
30 30 |
31 31 | ## Test split called on sliced string
missing_maxsplit_arg.py:29:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
@@ -115,8 +236,19 @@ missing_maxsplit_arg.py:29:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
30 |
31 | ## Test split called on sliced string
|
= help: Pass `maxsplit=1` into `str.rsplit()`
missing_maxsplit_arg.py:32:1: PLC0207 Pass `maxsplit=1` into `str.split()`
Safe fix
26 26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
27 27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
28 28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
29 |-Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
29 |+Foo.class_str.rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
30 30 |
31 31 | ## Test split called on sliced string
32 32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:32:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
31 | ## Test split called on sliced string
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
@@ -124,8 +256,19 @@ missing_maxsplit_arg.py:32:1: PLC0207 Pass `maxsplit=1` into `str.split()`
33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:33:1: PLC0207 Pass `maxsplit=1` into `str.split()`
Safe fix
29 29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
30 30 |
31 31 | ## Test split called on sliced string
32 |-"1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
32 |+"1,2,3"[::-1].split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
33 33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:33:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
31 | ## Test split called on sliced string
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
@@ -134,8 +277,19 @@ missing_maxsplit_arg.py:33:1: PLC0207 Pass `maxsplit=1` into `str.split()`
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:34:1: PLC0207 Pass `maxsplit=1` into `str.split()`
Safe fix
30 30 |
31 31 | ## Test split called on sliced string
32 32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
33 |-"1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
33 |+"1,2,3"[::-1][::-1].split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
34 34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:34:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
@@ -144,8 +298,19 @@ missing_maxsplit_arg.py:34:1: PLC0207 Pass `maxsplit=1` into `str.split()`
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:35:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
Safe fix
31 31 | ## Test split called on sliced string
32 32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
33 33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 |-SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
34 |+SEQ[:3].split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
35 35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:35:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
@@ -154,8 +319,19 @@ missing_maxsplit_arg.py:35:1: PLC0207 Instead of `str.split()`, call `str.rsplit
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
missing_maxsplit_arg.py:36:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
Safe fix
32 32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
33 33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 |-Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
35 |+Foo.class_str[1:3].rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
36 36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
38 38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:36:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
@@ -164,8 +340,19 @@ missing_maxsplit_arg.py:36:1: PLC0207 Instead of `str.rsplit()`, call `str.split
37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
missing_maxsplit_arg.py:37:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
Safe fix
33 33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 |-"1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
36 |+"1,2,3"[::-1].split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
37 37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
38 38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
39 39 |
missing_maxsplit_arg.py:37:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
@@ -173,8 +360,19 @@ missing_maxsplit_arg.py:37:1: PLC0207 Instead of `str.rsplit()`, call `str.split
| ^^^^^^^^^^^^^^^^^^^^^^ PLC0207
38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
missing_maxsplit_arg.py:38:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
Safe fix
34 34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 |-SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
37 |+SEQ[:3].split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
38 38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
39 39 |
40 40 | ## Test sep given as named argument
missing_maxsplit_arg.py:38:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
@@ -183,8 +381,19 @@ missing_maxsplit_arg.py:38:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
39 |
40 | ## Test sep given as named argument
|
= help: Pass `maxsplit=1` into `str.rsplit()`
missing_maxsplit_arg.py:41:1: PLC0207 Pass `maxsplit=1` into `str.split()`
Safe fix
35 35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
38 |-Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
38 |+Foo.class_str[1:3].rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
39 39 |
40 40 | ## Test sep given as named argument
41 41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:41:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
40 | ## Test sep given as named argument
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
@@ -192,8 +401,19 @@ missing_maxsplit_arg.py:41:1: PLC0207 Pass `maxsplit=1` into `str.split()`
42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:42:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
Safe fix
38 38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
39 39 |
40 40 | ## Test sep given as named argument
41 |-"1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
41 |+"1,2,3".split(maxsplit=1, sep=",")[0] # [missing-maxsplit-arg]
42 42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
43 43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
44 44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:42:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
40 | ## Test sep given as named argument
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
@@ -202,8 +422,19 @@ missing_maxsplit_arg.py:42:1: PLC0207 Instead of `str.split()`, call `str.rsplit
43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
missing_maxsplit_arg.py:43:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
Safe fix
39 39 |
40 40 | ## Test sep given as named argument
41 41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
42 |-"1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
42 |+"1,2,3".rsplit(maxsplit=1, sep=",")[-1] # [missing-maxsplit-arg]
43 43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
44 44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
45 45 |
missing_maxsplit_arg.py:43:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
@@ -211,8 +442,19 @@ missing_maxsplit_arg.py:43:1: PLC0207 Instead of `str.rsplit()`, call `str.split
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
missing_maxsplit_arg.py:44:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
Safe fix
40 40 | ## Test sep given as named argument
41 41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
42 42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
43 |-"1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
43 |+"1,2,3".split(maxsplit=1, sep=",")[0] # [missing-maxsplit-arg]
44 44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
45 45 |
46 46 | ## Special cases
missing_maxsplit_arg.py:44:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
@@ -221,8 +463,19 @@ missing_maxsplit_arg.py:44:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
45 |
46 | ## Special cases
|
= help: Pass `maxsplit=1` into `str.rsplit()`
missing_maxsplit_arg.py:47:1: PLC0207 Pass `maxsplit=1` into `str.split()`
Safe fix
41 41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
42 42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
43 43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
44 |-"1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
44 |+"1,2,3".rsplit(maxsplit=1, sep=",")[-1] # [missing-maxsplit-arg]
45 45 |
46 46 | ## Special cases
47 47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:47:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
46 | ## Special cases
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
@@ -230,8 +483,19 @@ missing_maxsplit_arg.py:47:1: PLC0207 Pass `maxsplit=1` into `str.split()`
48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg]
49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:48:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
Safe fix
44 44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
45 45 |
46 46 | ## Special cases
47 |-"1,2,3".split("\n")[0] # [missing-maxsplit-arg]
47 |+"1,2,3".split("\n", maxsplit=1)[0] # [missing-maxsplit-arg]
48 48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg]
49 49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
50 50 |
missing_maxsplit_arg.py:48:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
46 | ## Special cases
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
@@ -239,8 +503,19 @@ missing_maxsplit_arg.py:48:1: PLC0207 Instead of `str.split()`, call `str.rsplit
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
missing_maxsplit_arg.py:49:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
Safe fix
45 45 |
46 46 | ## Special cases
47 47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
48 |-"1,2,3".split("split")[-1] # [missing-maxsplit-arg]
48 |+"1,2,3".rsplit("split", maxsplit=1)[-1] # [missing-maxsplit-arg]
49 49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
50 50 |
51 51 | ## Test class attribute named split
missing_maxsplit_arg.py:49:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg]
@@ -249,8 +524,19 @@ missing_maxsplit_arg.py:49:1: PLC0207 Instead of `str.rsplit()`, call `str.split
50 |
51 | ## Test class attribute named split
|
= help: Use `str.split()` and pass `maxsplit=1`
missing_maxsplit_arg.py:52:1: PLC0207 Pass `maxsplit=1` into `str.split()`
Safe fix
46 46 | ## Special cases
47 47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
48 48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg]
49 |-"1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
49 |+"1,2,3".split("rsplit", maxsplit=1)[0] # [missing-maxsplit-arg]
50 50 |
51 51 | ## Test class attribute named split
52 52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:52:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
51 | ## Test class attribute named split
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
@@ -258,8 +544,19 @@ missing_maxsplit_arg.py:52:1: PLC0207 Pass `maxsplit=1` into `str.split()`
53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:53:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
Safe fix
49 49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
50 50 |
51 51 | ## Test class attribute named split
52 |-Bar.split.split(",")[0] # [missing-maxsplit-arg]
52 |+Bar.split.split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
53 53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
54 54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
55 55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:53:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
51 | ## Test class attribute named split
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
@@ -268,8 +565,19 @@ missing_maxsplit_arg.py:53:1: PLC0207 Instead of `str.split()`, call `str.rsplit
54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
missing_maxsplit_arg.py:54:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
Safe fix
50 50 |
51 51 | ## Test class attribute named split
52 52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
53 |-Bar.split.split(",")[-1] # [missing-maxsplit-arg]
53 |+Bar.split.rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
54 54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
55 55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
56 56 |
missing_maxsplit_arg.py:54:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
@@ -277,8 +585,19 @@ missing_maxsplit_arg.py:54:1: PLC0207 Instead of `str.rsplit()`, call `str.split
| ^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
missing_maxsplit_arg.py:55:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
Safe fix
51 51 | ## Test class attribute named split
52 52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
53 53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
54 |-Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
54 |+Bar.split.split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
55 55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
56 56 |
57 57 | ## Test unpacked dict literal kwargs
missing_maxsplit_arg.py:55:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
|
53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
@@ -287,15 +606,37 @@ missing_maxsplit_arg.py:55:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
56 |
57 | ## Test unpacked dict literal kwargs
|
= help: Pass `maxsplit=1` into `str.rsplit()`
missing_maxsplit_arg.py:58:1: PLC0207 Pass `maxsplit=1` into `str.split()`
Safe fix
52 52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
53 53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
54 54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
55 |-Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
55 |+Bar.split.rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
56 56 |
57 57 | ## Test unpacked dict literal kwargs
58 58 | "1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:58:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
57 | ## Test unpacked dict literal kwargs
58 | "1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
|
= help: Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:179:1: PLC0207 Pass `maxsplit=1` into `str.split()`
Unsafe fix
55 55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
56 56 |
57 57 | ## Test unpacked dict literal kwargs
58 |-"1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg]
58 |+"1,2,3".split(maxsplit=1, **{"sep": ","})[0] # [missing-maxsplit-arg]
59 59 |
60 60 |
61 61 | # OK
missing_maxsplit_arg.py:179:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
177 | # Errors
178 | kwargs_without_maxsplit = {"seq": ","}
@@ -304,8 +645,19 @@ missing_maxsplit_arg.py:179:1: PLC0207 Pass `maxsplit=1` into `str.split()`
180 | # OK
181 | kwargs_with_maxsplit = {"maxsplit": 1}
|
= help: Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:182:1: PLC0207 Pass `maxsplit=1` into `str.split()`
Unsafe fix
176 176 | ## TODO: These require the ability to resolve a dict variable name to a value
177 177 | # Errors
178 178 | kwargs_without_maxsplit = {"seq": ","}
179 |-"1,2,3".split(**kwargs_without_maxsplit)[0] # TODO: [missing-maxsplit-arg]
179 |+"1,2,3".split(maxsplit=1, **kwargs_without_maxsplit)[0] # TODO: [missing-maxsplit-arg]
180 180 | # OK
181 181 | kwargs_with_maxsplit = {"maxsplit": 1}
182 182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive
missing_maxsplit_arg.py:182:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
180 | # OK
181 | kwargs_with_maxsplit = {"maxsplit": 1}
@@ -314,11 +666,29 @@ missing_maxsplit_arg.py:182:1: PLC0207 Pass `maxsplit=1` into `str.split()`
183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}
184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
|
= help: Pass `maxsplit=1` into `str.split()`
missing_maxsplit_arg.py:184:1: PLC0207 Pass `maxsplit=1` into `str.split()`
Unsafe fix
179 179 | "1,2,3".split(**kwargs_without_maxsplit)[0] # TODO: [missing-maxsplit-arg]
180 180 | # OK
181 181 | kwargs_with_maxsplit = {"maxsplit": 1}
182 |-"1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive
182 |+"1,2,3".split(",", maxsplit=1, **kwargs_with_maxsplit)[0] # TODO: false positive
183 183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}
184 184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
missing_maxsplit_arg.py:184:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
|
182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive
183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}
184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
|
= help: Pass `maxsplit=1` into `str.split()`
Unsafe fix
181 181 | kwargs_with_maxsplit = {"maxsplit": 1}
182 182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive
183 183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}
184 |-"1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
184 |+"1,2,3".split(maxsplit=1, **kwargs_with_maxsplit)[0] # TODO: false positive

View File

@@ -104,6 +104,13 @@ pub(crate) fn reimplemented_operator(checker: &Checker, target: &FunctionLike) {
return;
}
// Skip decorated functions
if let FunctionLike::Function(func) = target {
if !func.decorator_list.is_empty() {
return;
}
}
let Some(params) = target.parameters() else {
return;
};

View File

@@ -180,18 +180,18 @@ fn is_valid_argument_type(
typing::is_float(binding, semantic),
)
})
.unwrap_or_default()
.unwrap_or((false, false))
} else {
(false, false)
};
match (method_name, constructor) {
// Decimal.from_float accepts int, bool, float
// Decimal.from_float: Only int or bool are safe (float is unsafe due to FloatOperation trap)
(MethodName::FromFloat, Constructor::Decimal) => match resolved_type {
ResolvedPythonType::Atom(PythonType::Number(
NumberLike::Integer | NumberLike::Bool | NumberLike::Float,
NumberLike::Integer | NumberLike::Bool,
)) => true,
ResolvedPythonType::Unknown => is_int || is_float,
ResolvedPythonType::Unknown => is_int,
_ => false,
},
// Fraction.from_float accepts int, bool, float
@@ -286,10 +286,8 @@ fn handle_non_finite_float_special_case(
let [float_arg] = arguments.args.as_ref() else {
return None;
};
as_non_finite_float_string_literal(float_arg)?;
let replacement_arg = checker.locator().slice(float_arg).to_string();
let replacement_text = format!("{constructor_name}({replacement_arg})");
let normalized = as_non_finite_float_string_literal(float_arg)?;
let replacement_text = format!(r#"{constructor_name}("{normalized}")"#);
Some(Edit::range_replacement(replacement_text, call.range()))
}

View File

@@ -158,7 +158,7 @@ FURB164.py:13:27: FURB164 [*] Verbose method `from_float` in `Decimal` construct
|
= help: Replace with `Decimal` constructor
Safe fix
Unsafe fix
10 10 | _ = fractions.Fraction.from_float(4.2)
11 11 | _ = Fraction.from_decimal(Decimal("4.2"))
12 12 | _ = Fraction.from_decimal(Decimal("-4.2"))
@@ -179,7 +179,7 @@ FURB164.py:14:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
|
= help: Replace with `Decimal` constructor
Safe fix
Unsafe fix
11 11 | _ = Fraction.from_decimal(Decimal("4.2"))
12 12 | _ = Fraction.from_decimal(Decimal("-4.2"))
13 13 | _ = Fraction.from_decimal(Decimal.from_float(4.2))
@@ -200,7 +200,7 @@ FURB164.py:15:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
|
= help: Replace with `Decimal` constructor
Safe fix
Unsafe fix
12 12 | _ = Fraction.from_decimal(Decimal("-4.2"))
13 13 | _ = Fraction.from_decimal(Decimal.from_float(4.2))
14 14 | _ = Decimal.from_float(0.1)
@@ -221,7 +221,7 @@ FURB164.py:16:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
|
= help: Replace with `Decimal` constructor
Safe fix
Unsafe fix
13 13 | _ = Fraction.from_decimal(Decimal.from_float(4.2))
14 14 | _ = Decimal.from_float(0.1)
15 15 | _ = Decimal.from_float(-0.5)
@@ -242,7 +242,7 @@ FURB164.py:17:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
|
= help: Replace with `Decimal` constructor
Safe fix
Unsafe fix
14 14 | _ = Decimal.from_float(0.1)
15 15 | _ = Decimal.from_float(-0.5)
16 16 | _ = Decimal.from_float(5.0)
@@ -310,7 +310,7 @@ FURB164.py:20:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
18 18 | _ = Decimal.from_float(float("inf"))
19 19 | _ = Decimal.from_float(float("-inf"))
20 |-_ = Decimal.from_float(float("Infinity"))
20 |+_ = Decimal("Infinity")
20 |+_ = Decimal("infinity")
21 21 | _ = Decimal.from_float(float("-Infinity"))
22 22 | _ = Decimal.from_float(float("nan"))
23 23 | _ = Decimal.from_float(float("-NaN"))
@@ -331,7 +331,7 @@ FURB164.py:21:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
19 19 | _ = Decimal.from_float(float("-inf"))
20 20 | _ = Decimal.from_float(float("Infinity"))
21 |-_ = Decimal.from_float(float("-Infinity"))
21 |+_ = Decimal("-Infinity")
21 |+_ = Decimal("-infinity")
22 22 | _ = Decimal.from_float(float("nan"))
23 23 | _ = Decimal.from_float(float("-NaN"))
24 24 | _ = Decimal.from_float(float(" \n+nan \t"))
@@ -373,7 +373,7 @@ FURB164.py:23:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
21 21 | _ = Decimal.from_float(float("-Infinity"))
22 22 | _ = Decimal.from_float(float("nan"))
23 |-_ = Decimal.from_float(float("-NaN"))
23 |+_ = Decimal("-NaN")
23 |+_ = Decimal("-nan")
24 24 | _ = Decimal.from_float(float(" \n+nan \t"))
25 25 | _ = Decimal.from_float(float(" iNf\n\t "))
26 26 | _ = Decimal.from_float(float(" -inF\n \t"))
@@ -394,7 +394,7 @@ FURB164.py:24:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
22 22 | _ = Decimal.from_float(float("nan"))
23 23 | _ = Decimal.from_float(float("-NaN"))
24 |-_ = Decimal.from_float(float(" \n+nan \t"))
24 |+_ = Decimal(" \n+nan \t")
24 |+_ = Decimal("+nan")
25 25 | _ = Decimal.from_float(float(" iNf\n\t "))
26 26 | _ = Decimal.from_float(float(" -inF\n \t"))
27 27 | _ = Decimal.from_float(float(" InfinIty\n\t "))
@@ -415,7 +415,7 @@ FURB164.py:25:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
23 23 | _ = Decimal.from_float(float("-NaN"))
24 24 | _ = Decimal.from_float(float(" \n+nan \t"))
25 |-_ = Decimal.from_float(float(" iNf\n\t "))
25 |+_ = Decimal(" iNf\n\t ")
25 |+_ = Decimal("inf")
26 26 | _ = Decimal.from_float(float(" -inF\n \t"))
27 27 | _ = Decimal.from_float(float(" InfinIty\n\t "))
28 28 | _ = Decimal.from_float(float(" -InfinIty\n \t"))
@@ -436,7 +436,7 @@ FURB164.py:26:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
24 24 | _ = Decimal.from_float(float(" \n+nan \t"))
25 25 | _ = Decimal.from_float(float(" iNf\n\t "))
26 |-_ = Decimal.from_float(float(" -inF\n \t"))
26 |+_ = Decimal(" -inF\n \t")
26 |+_ = Decimal("-inf")
27 27 | _ = Decimal.from_float(float(" InfinIty\n\t "))
28 28 | _ = Decimal.from_float(float(" -InfinIty\n \t"))
29 29 |
@@ -456,7 +456,7 @@ FURB164.py:27:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
25 25 | _ = Decimal.from_float(float(" iNf\n\t "))
26 26 | _ = Decimal.from_float(float(" -inF\n \t"))
27 |-_ = Decimal.from_float(float(" InfinIty\n\t "))
27 |+_ = Decimal(" InfinIty\n\t ")
27 |+_ = Decimal("infinity")
28 28 | _ = Decimal.from_float(float(" -InfinIty\n \t"))
29 29 |
30 30 | # Cases with keyword arguments - should produce unsafe fixes
@@ -477,7 +477,7 @@ FURB164.py:28:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
26 26 | _ = Decimal.from_float(float(" -inF\n \t"))
27 27 | _ = Decimal.from_float(float(" InfinIty\n\t "))
28 |-_ = Decimal.from_float(float(" -InfinIty\n \t"))
28 |+_ = Decimal(" -InfinIty\n \t")
28 |+_ = Decimal("-infinity")
29 29 |
30 30 | # Cases with keyword arguments - should produce unsafe fixes
31 31 | _ = Fraction.from_decimal(dec=Decimal("4.2"))
@@ -612,3 +612,96 @@ FURB164.py:45:5: FURB164 [*] Verbose method `from_float` in `Fraction` construct
46 46 |
47 47 | # OK - should not trigger the rule
48 48 | _ = Fraction(0.1)
FURB164.py:60:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction
|
59 | # Cases with int and bool - should produce safe fixes
60 | _ = Decimal.from_float(1)
| ^^^^^^^^^^^^^^^^^^^^^ FURB164
61 | _ = Decimal.from_float(True)
|
= help: Replace with `Decimal` constructor
Safe fix
57 57 | _ = decimal.Decimal(4.2)
58 58 |
59 59 | # Cases with int and bool - should produce safe fixes
60 |-_ = Decimal.from_float(1)
60 |+_ = Decimal(1)
61 61 | _ = Decimal.from_float(True)
62 62 |
63 63 | # Cases with non-finite floats - should produce safe fixes
FURB164.py:61:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction
|
59 | # Cases with int and bool - should produce safe fixes
60 | _ = Decimal.from_float(1)
61 | _ = Decimal.from_float(True)
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB164
62 |
63 | # Cases with non-finite floats - should produce safe fixes
|
= help: Replace with `Decimal` constructor
Safe fix
58 58 |
59 59 | # Cases with int and bool - should produce safe fixes
60 60 | _ = Decimal.from_float(1)
61 |-_ = Decimal.from_float(True)
61 |+_ = Decimal(True)
62 62 |
63 63 | # Cases with non-finite floats - should produce safe fixes
64 64 | _ = Decimal.from_float(float("-nan"))
FURB164.py:64:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction
|
63 | # Cases with non-finite floats - should produce safe fixes
64 | _ = Decimal.from_float(float("-nan"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164
65 | _ = Decimal.from_float(float("\x2dnan"))
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
|
= help: Replace with `Decimal` constructor
Safe fix
61 61 | _ = Decimal.from_float(True)
62 62 |
63 63 | # Cases with non-finite floats - should produce safe fixes
64 |-_ = Decimal.from_float(float("-nan"))
64 |+_ = Decimal("-nan")
65 65 | _ = Decimal.from_float(float("\x2dnan"))
66 66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
FURB164.py:65:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction
|
63 | # Cases with non-finite floats - should produce safe fixes
64 | _ = Decimal.from_float(float("-nan"))
65 | _ = Decimal.from_float(float("\x2dnan"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
|
= help: Replace with `Decimal` constructor
Safe fix
62 62 |
63 63 | # Cases with non-finite floats - should produce safe fixes
64 64 | _ = Decimal.from_float(float("-nan"))
65 |-_ = Decimal.from_float(float("\x2dnan"))
65 |+_ = Decimal("-nan")
66 66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
FURB164.py:66:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction
|
64 | _ = Decimal.from_float(float("-nan"))
65 | _ = Decimal.from_float(float("\x2dnan"))
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164
|
= help: Replace with `Decimal` constructor
Safe fix
63 63 | # Cases with non-finite floats - should produce safe fixes
64 64 | _ = Decimal.from_float(float("-nan"))
65 65 | _ = Decimal.from_float(float("\x2dnan"))
66 |-_ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
66 |+_ = Decimal("-nan")

View File

@@ -555,6 +555,44 @@ mod tests {
Ok(())
}
#[test_case(Rule::UnrawRePattern, Path::new("RUF039_py_version_sensitive.py"))]
fn preview_rules_py37(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__py37__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("ruff").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
unresolved_target_version: PythonVersion::PY37.into(),
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::UnrawRePattern, Path::new("RUF039_py_version_sensitive.py"))]
fn preview_rules_py38(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__py38__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("ruff").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
unresolved_target_version: PythonVersion::PY38.into(),
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"), r"^_+", 1)]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"), r"", 2)]
fn custom_regexp_preset(

View File

@@ -178,11 +178,10 @@ pub(crate) fn ambiguous_unicode_character_comment(
context: &LintContext,
locator: &Locator,
range: TextRange,
settings: &LinterSettings,
) {
let text = locator.slice(range);
for candidate in ambiguous_unicode_character(text, range, settings) {
candidate.into_diagnostic(Context::Comment, settings, context);
for candidate in ambiguous_unicode_character(text, range, context.settings()) {
candidate.into_diagnostic(Context::Comment, context);
}
}
@@ -342,13 +341,12 @@ impl Candidate {
}
}
fn into_diagnostic(
self,
context: Context,
settings: &LinterSettings,
lint_context: &LintContext,
) {
if !settings.allowed_confusables.contains(&self.confusable) {
fn into_diagnostic(self, context: Context, lint_context: &LintContext) {
if !lint_context
.settings()
.allowed_confusables
.contains(&self.confusable)
{
let char_range = TextRange::at(self.offset, self.confusable.text_len());
match context {
Context::String => lint_context.report_diagnostic_if_enabled(

View File

@@ -2,6 +2,7 @@ use anyhow::Context;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_semantic::{Scope, ScopeKind};
use ruff_python_trivia::{indentation_at_offset, textwrap};
use ruff_source_file::LineRanges;
@@ -117,13 +118,7 @@ pub(crate) fn post_init_default(checker: &Checker, function_def: &ast::StmtFunct
if !stopped_fixes {
diagnostic.try_set_fix(|| {
use_initvar(
current_scope,
function_def,
&parameter.parameter,
default,
checker,
)
use_initvar(current_scope, function_def, parameter, default, checker)
});
// Need to stop fixes as soon as there is a parameter we cannot fix.
// Otherwise, we risk a syntax error (a parameter without a default
@@ -138,10 +133,11 @@ pub(crate) fn post_init_default(checker: &Checker, function_def: &ast::StmtFunct
fn use_initvar(
current_scope: &Scope,
post_init_def: &ast::StmtFunctionDef,
parameter: &ast::Parameter,
parameter_with_default: &ast::ParameterWithDefault,
default: &ast::Expr,
checker: &Checker,
) -> anyhow::Result<Fix> {
let parameter = &parameter_with_default.parameter;
if current_scope.has(&parameter.name) {
return Err(anyhow::anyhow!(
"Cannot add a `{}: InitVar` field to the class body, as a field by that name already exists",
@@ -157,17 +153,25 @@ fn use_initvar(
checker.semantic(),
)?;
let locator = checker.locator();
let default_loc = parenthesized_range(
default.into(),
parameter_with_default.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(default.range());
// Delete the default value. For example,
// - def __post_init__(self, foo: int = 0) -> None: ...
// + def __post_init__(self, foo: int) -> None: ...
let default_edit = Edit::deletion(parameter.end(), default.end());
let default_edit = Edit::deletion(parameter.end(), default_loc.end());
// Add `dataclasses.InitVar` field to class body.
let locator = checker.locator();
let content = {
let default = locator.slice(default_loc);
let parameter_name = locator.slice(&parameter.name);
let default = locator.slice(default);
let line_ending = checker.stylist().line_ending().as_str();
if let Some(annotation) = &parameter

View File

@@ -1,13 +1,13 @@
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{
BytesLiteral, Expr, ExprBytesLiteral, ExprCall, ExprStringLiteral, StringLiteral,
BytesLiteral, Expr, ExprBytesLiteral, ExprCall, ExprStringLiteral, PythonVersion, StringLiteral,
};
use ruff_python_semantic::{Modules, SemanticModel};
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::{Edit, Fix, FixAvailability, Violation};
@@ -24,6 +24,29 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// Regular expressions should be written
/// using raw strings to avoid double escaping.
///
/// ## Fix safety
/// The fix is unsafe if the string/bytes literal contains an escape sequence because the fix alters
/// the runtime value of the literal while retaining the regex semantics.
///
/// For example
/// ```python
/// # Literal is `1\n2`.
/// re.compile("1\n2")
///
/// # Literal is `1\\n2`, but the regex library will interpret `\\n` and will still match a newline
/// # character as before.
/// re.compile(r"1\n2")
/// ```
///
/// ## Fix availability
/// A fix is not available if either
/// * the argument is a string with a (no-op) `u` prefix (e.g., `u"foo"`) as the prefix is
/// incompatible with the raw prefix `r`
/// * the argument is a string or bytes literal with an escape sequence that has a different
/// meaning in the context of a regular expression such as `\b`, which is word boundary or
/// backspace in a regex, depending on the context, but always a backspace in string and bytes
/// literals.
///
/// ## Example
///
/// ```python
@@ -163,20 +186,44 @@ fn check_string(checker: &Checker, literal: &StringLiteral, module: RegexModule,
let range = literal.range;
let mut diagnostic = checker.report_diagnostic(UnrawRePattern { module, func, kind }, range);
if
// The (no-op) `u` prefix is a syntax error when combined with `r`
!literal.flags.prefix().is_unicode()
// We are looking for backslash characters
// in the raw source code here, because `\n`
// gets converted to a single character already
// at the lexing stage.
&&!checker.locator().slice(literal.range()).contains('\\')
{
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
"r".to_string(),
literal.range().start(),
)));
let Some(applicability) = raw_string_applicability(checker, literal) else {
return;
};
diagnostic.set_fix(Fix::applicable_edit(
Edit::insertion("r".to_string(), literal.range().start()),
applicability,
));
}
/// Check how safe it is to prepend the `r` prefix to the string.
///
/// ## Returns
/// * `None` if the prefix cannot be added,
/// * `Some(a)` if it can be added with applicability `a`.
fn raw_string_applicability(checker: &Checker, literal: &StringLiteral) -> Option<Applicability> {
if literal.flags.prefix().is_unicode() {
// The (no-op) `u` prefix is a syntax error when combined with `r`
return None;
}
if checker.target_version() >= PythonVersion::PY38 {
raw_applicability(checker, literal.range(), |escaped| {
matches!(
escaped,
Some('a' | 'f' | 'n' | 'r' | 't' | 'u' | 'U' | 'v' | 'x' | 'N')
)
})
} else {
raw_applicability(checker, literal.range(), |escaped| {
matches!(
escaped,
Some('a' | 'f' | 'n' | 'r' | 't' | 'u' | 'U' | 'v' | 'x')
)
})
}
// re.compile("\a\f\n\N{Partial Differential}\r\t\u27F2\U0001F0A1\v\x41") # with unsafe fix
}
fn check_bytes(checker: &Checker, literal: &BytesLiteral, module: RegexModule, func: &str) {
@@ -187,5 +234,53 @@ fn check_bytes(checker: &Checker, literal: &BytesLiteral, module: RegexModule, f
let kind = PatternKind::Bytes;
let func = func.to_string();
let range = literal.range;
checker.report_diagnostic(UnrawRePattern { module, func, kind }, range);
let mut diagnostic = checker.report_diagnostic(UnrawRePattern { module, func, kind }, range);
let Some(applicability) = raw_byte_applicability(checker, literal) else {
return;
};
diagnostic.set_fix(Fix::applicable_edit(
Edit::insertion("r".to_string(), literal.range().start()),
applicability,
));
}
/// Check how same it is to prepend the `r` prefix to the byte sting.
///
/// ## Returns
/// * `None` if the prefix cannot be added,
/// * `Some(a)` if it can be added with applicability `a`.
fn raw_byte_applicability(checker: &Checker, literal: &BytesLiteral) -> Option<Applicability> {
raw_applicability(checker, literal.range(), |escaped| {
matches!(escaped, Some('a' | 'f' | 'n' | 'r' | 't' | 'v' | 'x'))
})
}
fn raw_applicability(
checker: &Checker,
literal_range: TextRange,
match_allowed_escape_sequence: impl Fn(Option<char>) -> bool,
) -> Option<Applicability> {
let mut found_slash = false;
let mut chars = checker.locator().slice(literal_range).chars().peekable();
while let Some(char) = chars.next() {
if char == '\\' {
found_slash = true;
// Turning `"\uXXXX"` into `r"\uXXXX"` is behaviorally equivalent when passed
// to `re`, however, it's not exactly the same runtime value.
// Similarly, for the other escape sequences.
if !match_allowed_escape_sequence(chars.peek().copied()) {
// If the next character is not one of the whitelisted ones, we likely cannot safely turn
// this into a raw string.
return None;
}
}
}
Some(if found_slash {
Applicability::Unsafe
} else {
Applicability::Safe
})
}

View File

@@ -156,3 +156,281 @@ RUF033.py:67:59: RUF033 `__post_init__` method with argument defaults
| ^^^^^ RUF033
|
= help: Use `dataclasses.InitVar` instead
RUF033.py:73:41: RUF033 [*] `__post_init__` method with argument defaults
|
71 | @dataclass
72 | class Foo:
73 | def __post_init__(self, bar: int = (x := 1)) -> None:
| ^^^^^^ RUF033
74 | pass
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
70 70 | # https://github.com/astral-sh/ruff/issues/18950
71 71 | @dataclass
72 72 | class Foo:
73 |- def __post_init__(self, bar: int = (x := 1)) -> None:
73 |+ bar: InitVar[int] = (x := 1)
74 |+ def __post_init__(self, bar: int) -> None:
74 75 | pass
75 76 |
76 77 |
RUF033.py:81:21: RUF033 [*] `__post_init__` method with argument defaults
|
79 | def __post_init__(
80 | self,
81 | bar: int = (x := 1) # comment
| ^^^^^^ RUF033
82 | ,
83 | baz: int = (y := 2), # comment
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
76 76 |
77 77 | @dataclass
78 78 | class Foo:
79 |+ bar: InitVar[int] = (x := 1)
79 80 | def __post_init__(
80 81 | self,
81 |- bar: int = (x := 1) # comment
82 |+ bar: int # comment
82 83 | ,
83 84 | baz: int = (y := 2), # comment
84 85 | foo = (a := 1) # comment
RUF033.py:83:21: RUF033 [*] `__post_init__` method with argument defaults
|
81 | bar: int = (x := 1) # comment
82 | ,
83 | baz: int = (y := 2), # comment
| ^^^^^^ RUF033
84 | foo = (a := 1) # comment
85 | ,
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
76 76 |
77 77 | @dataclass
78 78 | class Foo:
79 |+ baz: InitVar[int] = (y := 2)
79 80 | def __post_init__(
80 81 | self,
81 82 | bar: int = (x := 1) # comment
82 83 | ,
83 |- baz: int = (y := 2), # comment
84 |+ baz: int, # comment
84 85 | foo = (a := 1) # comment
85 86 | ,
86 87 | faz = (b := 2), # comment
RUF033.py:84:16: RUF033 [*] `__post_init__` method with argument defaults
|
82 | ,
83 | baz: int = (y := 2), # comment
84 | foo = (a := 1) # comment
| ^^^^^^ RUF033
85 | ,
86 | faz = (b := 2), # comment
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
76 76 |
77 77 | @dataclass
78 78 | class Foo:
79 |+ foo: InitVar = (a := 1)
79 80 | def __post_init__(
80 81 | self,
81 82 | bar: int = (x := 1) # comment
82 83 | ,
83 84 | baz: int = (y := 2), # comment
84 |- foo = (a := 1) # comment
85 |+ foo # comment
85 86 | ,
86 87 | faz = (b := 2), # comment
87 88 | ) -> None:
RUF033.py:86:16: RUF033 [*] `__post_init__` method with argument defaults
|
84 | foo = (a := 1) # comment
85 | ,
86 | faz = (b := 2), # comment
| ^^^^^^ RUF033
87 | ) -> None:
88 | pass
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
76 76 |
77 77 | @dataclass
78 78 | class Foo:
79 |+ faz: InitVar = (b := 2)
79 80 | def __post_init__(
80 81 | self,
81 82 | bar: int = (x := 1) # comment
--------------------------------------------------------------------------------
83 84 | baz: int = (y := 2), # comment
84 85 | foo = (a := 1) # comment
85 86 | ,
86 |- faz = (b := 2), # comment
87 |+ faz, # comment
87 88 | ) -> None:
88 89 | pass
89 90 |
RUF033.py:95:20: RUF033 [*] `__post_init__` method with argument defaults
|
93 | def __post_init__(
94 | self,
95 | bar: int = 1, # comment
| ^ RUF033
96 | baz: int = 2, # comment
97 | ) -> None:
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
90 90 |
91 91 | @dataclass
92 92 | class Foo:
93 |+ bar: InitVar[int] = 1
93 94 | def __post_init__(
94 95 | self,
95 |- bar: int = 1, # comment
96 |+ bar: int, # comment
96 97 | baz: int = 2, # comment
97 98 | ) -> None:
98 99 | pass
RUF033.py:96:20: RUF033 [*] `__post_init__` method with argument defaults
|
94 | self,
95 | bar: int = 1, # comment
96 | baz: int = 2, # comment
| ^ RUF033
97 | ) -> None:
98 | pass
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
90 90 |
91 91 | @dataclass
92 92 | class Foo:
93 |+ baz: InitVar[int] = 2
93 94 | def __post_init__(
94 95 | self,
95 96 | bar: int = 1, # comment
96 |- baz: int = 2, # comment
97 |+ baz: int, # comment
97 98 | ) -> None:
98 99 | pass
99 100 |
RUF033.py:105:22: RUF033 [*] `__post_init__` method with argument defaults
|
103 | def __post_init__(
104 | self,
105 | arg1: int = (1) # comment
| ^ RUF033
106 | ,
107 | arg2: int = ((1)) # comment
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
100 100 |
101 101 | @dataclass
102 102 | class Foo:
103 |+ arg1: InitVar[int] = (1)
103 104 | def __post_init__(
104 105 | self,
105 |- arg1: int = (1) # comment
106 |+ arg1: int # comment
106 107 | ,
107 108 | arg2: int = ((1)) # comment
108 109 | ,
RUF033.py:107:23: RUF033 [*] `__post_init__` method with argument defaults
|
105 | arg1: int = (1) # comment
106 | ,
107 | arg2: int = ((1)) # comment
| ^ RUF033
108 | ,
109 | arg2: int = (i for i in range(10)) # comment
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
100 100 |
101 101 | @dataclass
102 102 | class Foo:
103 |+ arg2: InitVar[int] = ((1))
103 104 | def __post_init__(
104 105 | self,
105 106 | arg1: int = (1) # comment
106 107 | ,
107 |- arg2: int = ((1)) # comment
108 |+ arg2: int # comment
108 109 | ,
109 110 | arg2: int = (i for i in range(10)) # comment
110 111 | ,
RUF033.py:109:21: RUF033 [*] `__post_init__` method with argument defaults
|
107 | arg2: int = ((1)) # comment
108 | ,
109 | arg2: int = (i for i in range(10)) # comment
| ^^^^^^^^^^^^^^^^^^^^^^ RUF033
110 | ,
111 | ) -> None:
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
100 100 |
101 101 | @dataclass
102 102 | class Foo:
103 |+ arg2: InitVar[int] = (i for i in range(10))
103 104 | def __post_init__(
104 105 | self,
105 106 | arg1: int = (1) # comment
106 107 | ,
107 108 | arg2: int = ((1)) # comment
108 109 | ,
109 |- arg2: int = (i for i in range(10)) # comment
110 |+ arg2: int # comment
110 111 | ,
111 112 | ) -> None:
112 113 | pass
RUF033.py:121:27: RUF033 [*] `__post_init__` method with argument defaults
|
119 | def __post_init__(
120 | self,
121 | bar: (int) = (yield from range(5)) # comment
| ^^^^^^^^^^^^^^^^^^^ RUF033
122 | ,
123 | ) -> None:
|
= help: Use `dataclasses.InitVar` instead
Unsafe fix
116 116 | def fun_with_python_syntax():
117 117 | @dataclass
118 118 | class Foo:
119 |+ bar: InitVar[int] = (yield from range(5))
119 120 | def __post_init__(
120 121 | self,
121 |- bar: (int) = (yield from range(5)) # comment
122 |+ bar: (int) # comment
122 123 | ,
123 124 | ) -> None:
124 125 | ...

View File

@@ -21,7 +21,7 @@ RUF039.py:5:12: RUF039 [*] First argument to `re.compile()` is not raw string
7 7 | re.finditer("dou\ble")
8 8 | re.fullmatch('''t\riple single''')
RUF039.py:6:12: RUF039 First argument to `re.findall()` is not raw string
RUF039.py:6:12: RUF039 [*] First argument to `re.findall()` is not raw string
|
4 | # Errors
5 | re.compile('single free-spacing', flags=re.X)
@@ -32,6 +32,16 @@ RUF039.py:6:12: RUF039 First argument to `re.findall()` is not raw string
|
= help: Replace with raw string
Unsafe fix
3 3 |
4 4 | # Errors
5 5 | re.compile('single free-spacing', flags=re.X)
6 |-re.findall('si\ngle')
6 |+re.findall(r'si\ngle')
7 7 | re.finditer("dou\ble")
8 8 | re.fullmatch('''t\riple single''')
9 9 | re.match("""\triple double""")
RUF039.py:7:13: RUF039 First argument to `re.finditer()` is not raw string
|
5 | re.compile('single free-spacing', flags=re.X)
@@ -43,7 +53,7 @@ RUF039.py:7:13: RUF039 First argument to `re.finditer()` is not raw string
|
= help: Replace with raw string
RUF039.py:8:14: RUF039 First argument to `re.fullmatch()` is not raw string
RUF039.py:8:14: RUF039 [*] First argument to `re.fullmatch()` is not raw string
|
6 | re.findall('si\ngle')
7 | re.finditer("dou\ble")
@@ -54,7 +64,17 @@ RUF039.py:8:14: RUF039 First argument to `re.fullmatch()` is not raw string
|
= help: Replace with raw string
RUF039.py:9:10: RUF039 First argument to `re.match()` is not raw string
Unsafe fix
5 5 | re.compile('single free-spacing', flags=re.X)
6 6 | re.findall('si\ngle')
7 7 | re.finditer("dou\ble")
8 |-re.fullmatch('''t\riple single''')
8 |+re.fullmatch(r'''t\riple single''')
9 9 | re.match("""\triple double""")
10 10 | re.search('two', 'args')
11 11 | re.split("raw", r'second')
RUF039.py:9:10: RUF039 [*] First argument to `re.match()` is not raw string
|
7 | re.finditer("dou\ble")
8 | re.fullmatch('''t\riple single''')
@@ -65,6 +85,16 @@ RUF039.py:9:10: RUF039 First argument to `re.match()` is not raw string
|
= help: Replace with raw string
Unsafe fix
6 6 | re.findall('si\ngle')
7 7 | re.finditer("dou\ble")
8 8 | re.fullmatch('''t\riple single''')
9 |-re.match("""\triple double""")
9 |+re.match(r"""\triple double""")
10 10 | re.search('two', 'args')
11 11 | re.split("raw", r'second')
12 12 | re.sub(u'''nicode''', u"f(?i)rst")
RUF039.py:10:11: RUF039 [*] First argument to `re.search()` is not raw string
|
8 | re.fullmatch('''t\riple single''')
@@ -117,7 +147,7 @@ RUF039.py:12:8: RUF039 First argument to `re.sub()` is not raw string
|
= help: Replace with raw string
RUF039.py:13:9: RUF039 First argument to `re.subn()` is not raw bytes literal
RUF039.py:13:9: RUF039 [*] First argument to `re.subn()` is not raw bytes literal
|
11 | re.split("raw", r'second')
12 | re.sub(u'''nicode''', u"f(?i)rst")
@@ -128,6 +158,16 @@ RUF039.py:13:9: RUF039 First argument to `re.subn()` is not raw bytes literal
|
= help: Replace with raw bytes literal
Safe fix
10 10 | re.search('two', 'args')
11 11 | re.split("raw", r'second')
12 12 | re.sub(u'''nicode''', u"f(?i)rst")
13 |-re.subn(b"""ytes are""", f"\u006e")
13 |+re.subn(rb"""ytes are""", f"\u006e")
14 14 |
15 15 | regex.compile('single free-spacing', flags=regex.X)
16 16 | regex.findall('si\ngle')
RUF039.py:15:15: RUF039 [*] First argument to `regex.compile()` is not raw string
|
13 | re.subn(b"""ytes are""", f"\u006e")
@@ -149,7 +189,7 @@ RUF039.py:15:15: RUF039 [*] First argument to `regex.compile()` is not raw strin
17 17 | regex.finditer("dou\ble")
18 18 | regex.fullmatch('''t\riple single''')
RUF039.py:16:15: RUF039 First argument to `regex.findall()` is not raw string
RUF039.py:16:15: RUF039 [*] First argument to `regex.findall()` is not raw string
|
15 | regex.compile('single free-spacing', flags=regex.X)
16 | regex.findall('si\ngle')
@@ -159,6 +199,16 @@ RUF039.py:16:15: RUF039 First argument to `regex.findall()` is not raw string
|
= help: Replace with raw string
Unsafe fix
13 13 | re.subn(b"""ytes are""", f"\u006e")
14 14 |
15 15 | regex.compile('single free-spacing', flags=regex.X)
16 |-regex.findall('si\ngle')
16 |+regex.findall(r'si\ngle')
17 17 | regex.finditer("dou\ble")
18 18 | regex.fullmatch('''t\riple single''')
19 19 | regex.match("""\triple double""")
RUF039.py:17:16: RUF039 First argument to `regex.finditer()` is not raw string
|
15 | regex.compile('single free-spacing', flags=regex.X)
@@ -170,7 +220,7 @@ RUF039.py:17:16: RUF039 First argument to `regex.finditer()` is not raw string
|
= help: Replace with raw string
RUF039.py:18:17: RUF039 First argument to `regex.fullmatch()` is not raw string
RUF039.py:18:17: RUF039 [*] First argument to `regex.fullmatch()` is not raw string
|
16 | regex.findall('si\ngle')
17 | regex.finditer("dou\ble")
@@ -181,7 +231,17 @@ RUF039.py:18:17: RUF039 First argument to `regex.fullmatch()` is not raw string
|
= help: Replace with raw string
RUF039.py:19:13: RUF039 First argument to `regex.match()` is not raw string
Unsafe fix
15 15 | regex.compile('single free-spacing', flags=regex.X)
16 16 | regex.findall('si\ngle')
17 17 | regex.finditer("dou\ble")
18 |-regex.fullmatch('''t\riple single''')
18 |+regex.fullmatch(r'''t\riple single''')
19 19 | regex.match("""\triple double""")
20 20 | regex.search('two', 'args')
21 21 | regex.split("raw", r'second')
RUF039.py:19:13: RUF039 [*] First argument to `regex.match()` is not raw string
|
17 | regex.finditer("dou\ble")
18 | regex.fullmatch('''t\riple single''')
@@ -192,6 +252,16 @@ RUF039.py:19:13: RUF039 First argument to `regex.match()` is not raw string
|
= help: Replace with raw string
Unsafe fix
16 16 | regex.findall('si\ngle')
17 17 | regex.finditer("dou\ble")
18 18 | regex.fullmatch('''t\riple single''')
19 |-regex.match("""\triple double""")
19 |+regex.match(r"""\triple double""")
20 20 | regex.search('two', 'args')
21 21 | regex.split("raw", r'second')
22 22 | regex.sub(u'''nicode''', u"f(?i)rst")
RUF039.py:20:14: RUF039 [*] First argument to `regex.search()` is not raw string
|
18 | regex.fullmatch('''t\riple single''')
@@ -244,7 +314,7 @@ RUF039.py:22:11: RUF039 First argument to `regex.sub()` is not raw string
|
= help: Replace with raw string
RUF039.py:23:12: RUF039 First argument to `regex.subn()` is not raw bytes literal
RUF039.py:23:12: RUF039 [*] First argument to `regex.subn()` is not raw bytes literal
|
21 | regex.split("raw", r'second')
22 | regex.sub(u'''nicode''', u"f(?i)rst")
@@ -255,6 +325,16 @@ RUF039.py:23:12: RUF039 First argument to `regex.subn()` is not raw bytes litera
|
= help: Replace with raw bytes literal
Safe fix
20 20 | regex.search('two', 'args')
21 21 | regex.split("raw", r'second')
22 22 | regex.sub(u'''nicode''', u"f(?i)rst")
23 |-regex.subn(b"""ytes are""", f"\u006e")
23 |+regex.subn(rb"""ytes are""", f"\u006e")
24 24 |
25 25 | regex.template("""(?m)
26 26 | (?:ulti)?
RUF039.py:25:16: RUF039 [*] First argument to `regex.template()` is not raw string
|
23 | regex.subn(b"""ytes are""", f"\u006e")
@@ -278,3 +358,111 @@ RUF039.py:25:16: RUF039 [*] First argument to `regex.template()` is not raw stri
26 26 | (?:ulti)?
27 27 | (?=(?<!(?<=(?!l)))
28 28 | l(?i:ne)
RUF039.py:59:12: RUF039 [*] First argument to `re.compile()` is not raw string
|
58 | # https://github.com/astral-sh/ruff/issues/16713
59 | re.compile("\a\f\n\r\t\u27F2\U0001F0A1\v\x41") # with unsafe fix
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF039
60 | re.compile("\b") # without fix
61 | re.compile("\"") # without fix
|
= help: Replace with raw string
Unsafe fix
56 56 |
57 57 |
58 58 | # https://github.com/astral-sh/ruff/issues/16713
59 |-re.compile("\a\f\n\r\t\u27F2\U0001F0A1\v\x41") # with unsafe fix
59 |+re.compile(r"\a\f\n\r\t\u27F2\U0001F0A1\v\x41") # with unsafe fix
60 60 | re.compile("\b") # without fix
61 61 | re.compile("\"") # without fix
62 62 | re.compile("\'") # without fix
RUF039.py:60:12: RUF039 First argument to `re.compile()` is not raw string
|
58 | # https://github.com/astral-sh/ruff/issues/16713
59 | re.compile("\a\f\n\r\t\u27F2\U0001F0A1\v\x41") # with unsafe fix
60 | re.compile("\b") # without fix
| ^^^^ RUF039
61 | re.compile("\"") # without fix
62 | re.compile("\'") # without fix
|
= help: Replace with raw string
RUF039.py:61:12: RUF039 First argument to `re.compile()` is not raw string
|
59 | re.compile("\a\f\n\r\t\u27F2\U0001F0A1\v\x41") # with unsafe fix
60 | re.compile("\b") # without fix
61 | re.compile("\"") # without fix
| ^^^^ RUF039
62 | re.compile("\'") # without fix
63 | re.compile('\"') # without fix
|
= help: Replace with raw string
RUF039.py:62:12: RUF039 First argument to `re.compile()` is not raw string
|
60 | re.compile("\b") # without fix
61 | re.compile("\"") # without fix
62 | re.compile("\'") # without fix
| ^^^^ RUF039
63 | re.compile('\"') # without fix
64 | re.compile('\'') # without fix
|
= help: Replace with raw string
RUF039.py:63:12: RUF039 First argument to `re.compile()` is not raw string
|
61 | re.compile("\"") # without fix
62 | re.compile("\'") # without fix
63 | re.compile('\"') # without fix
| ^^^^ RUF039
64 | re.compile('\'') # without fix
65 | re.compile("\\") # without fix
|
= help: Replace with raw string
RUF039.py:64:12: RUF039 First argument to `re.compile()` is not raw string
|
62 | re.compile("\'") # without fix
63 | re.compile('\"') # without fix
64 | re.compile('\'') # without fix
| ^^^^ RUF039
65 | re.compile("\\") # without fix
66 | re.compile("\101") # without fix
|
= help: Replace with raw string
RUF039.py:65:12: RUF039 First argument to `re.compile()` is not raw string
|
63 | re.compile('\"') # without fix
64 | re.compile('\'') # without fix
65 | re.compile("\\") # without fix
| ^^^^ RUF039
66 | re.compile("\101") # without fix
67 | re.compile("a\
|
= help: Replace with raw string
RUF039.py:66:12: RUF039 First argument to `re.compile()` is not raw string
|
64 | re.compile('\'') # without fix
65 | re.compile("\\") # without fix
66 | re.compile("\101") # without fix
| ^^^^^^ RUF039
67 | re.compile("a\
68 | b") # without fix
|
= help: Replace with raw string
RUF039.py:67:12: RUF039 First argument to `re.compile()` is not raw string
|
65 | re.compile("\\") # without fix
66 | re.compile("\101") # without fix
67 | re.compile("a\
| ____________^
68 | | b") # without fix
| |__^ RUF039
|
= help: Replace with raw string

View File

@@ -65,7 +65,7 @@ RUF039_concat.py:12:5: RUF039 [*] First argument to `re.findall()` is not raw st
14 14 | """
15 15 | )
RUF039_concat.py:26:5: RUF039 First argument to `re.match()` is not raw bytes literal
RUF039_concat.py:26:5: RUF039 [*] First argument to `re.match()` is not raw bytes literal
|
24 | )
25 | re.match(
@@ -76,6 +76,16 @@ RUF039_concat.py:26:5: RUF039 First argument to `re.match()` is not raw bytes li
|
= help: Replace with raw bytes literal
Safe fix
23 23 | f'much?'
24 24 | )
25 25 | re.match(
26 |- b'reak'
26 |+ rb'reak'
27 27 | br'eak'
28 28 | )
29 29 | re.search(
RUF039_concat.py:30:8: RUF039 First argument to `re.search()` is not raw string
|
28 | )
@@ -295,7 +305,7 @@ RUF039_concat.py:52:5: RUF039 [*] First argument to `regex.findall()` is not raw
54 54 | """
55 55 | )
RUF039_concat.py:66:5: RUF039 First argument to `regex.match()` is not raw bytes literal
RUF039_concat.py:66:5: RUF039 [*] First argument to `regex.match()` is not raw bytes literal
|
64 | )
65 | regex.match(
@@ -306,6 +316,16 @@ RUF039_concat.py:66:5: RUF039 First argument to `regex.match()` is not raw bytes
|
= help: Replace with raw bytes literal
Safe fix
63 63 | f'much?'
64 64 | )
65 65 | regex.match(
66 |- b'reak'
66 |+ rb'reak'
67 67 | br'eak'
68 68 | )
69 69 | regex.search(
RUF039_concat.py:70:8: RUF039 First argument to `regex.search()` is not raw string
|
68 | )
@@ -460,3 +480,223 @@ RUF039_concat.py:78:24: RUF039 [*] First argument to `regex.subn()` is not raw s
79 79 |
80 80 |
81 81 | regex.template(
RUF039_concat.py:98:5: RUF039 [*] First argument to `re.compile()` is not raw string
|
96 | # https://github.com/astral-sh/ruff/issues/16713
97 | re.compile(
98 | "["
| ^^^ RUF039
99 | "\U0001F600-\U0001F64F" # emoticons
100 | "\U0001F300-\U0001F5FF" # symbols & pictographs
|
= help: Replace with raw string
Safe fix
95 95 |
96 96 | # https://github.com/astral-sh/ruff/issues/16713
97 97 | re.compile(
98 |- "["
98 |+ r"["
99 99 | "\U0001F600-\U0001F64F" # emoticons
100 100 | "\U0001F300-\U0001F5FF" # symbols & pictographs
101 101 | "\U0001F680-\U0001F6FF" # transport & map symbols
RUF039_concat.py:99:5: RUF039 [*] First argument to `re.compile()` is not raw string
|
97 | re.compile(
98 | "["
99 | "\U0001F600-\U0001F64F" # emoticons
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF039
100 | "\U0001F300-\U0001F5FF" # symbols & pictographs
101 | "\U0001F680-\U0001F6FF" # transport & map symbols
|
= help: Replace with raw string
Unsafe fix
96 96 | # https://github.com/astral-sh/ruff/issues/16713
97 97 | re.compile(
98 98 | "["
99 |- "\U0001F600-\U0001F64F" # emoticons
99 |+ r"\U0001F600-\U0001F64F" # emoticons
100 100 | "\U0001F300-\U0001F5FF" # symbols & pictographs
101 101 | "\U0001F680-\U0001F6FF" # transport & map symbols
102 102 | "\U0001F1E0-\U0001F1FF" # flags (iOS)
RUF039_concat.py:100:5: RUF039 [*] First argument to `re.compile()` is not raw string
|
98 | "["
99 | "\U0001F600-\U0001F64F" # emoticons
100 | "\U0001F300-\U0001F5FF" # symbols & pictographs
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF039
101 | "\U0001F680-\U0001F6FF" # transport & map symbols
102 | "\U0001F1E0-\U0001F1FF" # flags (iOS)
|
= help: Replace with raw string
Unsafe fix
97 97 | re.compile(
98 98 | "["
99 99 | "\U0001F600-\U0001F64F" # emoticons
100 |- "\U0001F300-\U0001F5FF" # symbols & pictographs
100 |+ r"\U0001F300-\U0001F5FF" # symbols & pictographs
101 101 | "\U0001F680-\U0001F6FF" # transport & map symbols
102 102 | "\U0001F1E0-\U0001F1FF" # flags (iOS)
103 103 | "\U00002702-\U000027B0"
RUF039_concat.py:101:5: RUF039 [*] First argument to `re.compile()` is not raw string
|
99 | "\U0001F600-\U0001F64F" # emoticons
100 | "\U0001F300-\U0001F5FF" # symbols & pictographs
101 | "\U0001F680-\U0001F6FF" # transport & map symbols
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF039
102 | "\U0001F1E0-\U0001F1FF" # flags (iOS)
103 | "\U00002702-\U000027B0"
|
= help: Replace with raw string
Unsafe fix
98 98 | "["
99 99 | "\U0001F600-\U0001F64F" # emoticons
100 100 | "\U0001F300-\U0001F5FF" # symbols & pictographs
101 |- "\U0001F680-\U0001F6FF" # transport & map symbols
101 |+ r"\U0001F680-\U0001F6FF" # transport & map symbols
102 102 | "\U0001F1E0-\U0001F1FF" # flags (iOS)
103 103 | "\U00002702-\U000027B0"
104 104 | "\U000024C2-\U0001F251"
RUF039_concat.py:102:5: RUF039 [*] First argument to `re.compile()` is not raw string
|
100 | "\U0001F300-\U0001F5FF" # symbols & pictographs
101 | "\U0001F680-\U0001F6FF" # transport & map symbols
102 | "\U0001F1E0-\U0001F1FF" # flags (iOS)
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF039
103 | "\U00002702-\U000027B0"
104 | "\U000024C2-\U0001F251"
|
= help: Replace with raw string
Unsafe fix
99 99 | "\U0001F600-\U0001F64F" # emoticons
100 100 | "\U0001F300-\U0001F5FF" # symbols & pictographs
101 101 | "\U0001F680-\U0001F6FF" # transport & map symbols
102 |- "\U0001F1E0-\U0001F1FF" # flags (iOS)
102 |+ r"\U0001F1E0-\U0001F1FF" # flags (iOS)
103 103 | "\U00002702-\U000027B0"
104 104 | "\U000024C2-\U0001F251"
105 105 | "\u200d" # zero width joiner
RUF039_concat.py:103:5: RUF039 [*] First argument to `re.compile()` is not raw string
|
101 | "\U0001F680-\U0001F6FF" # transport & map symbols
102 | "\U0001F1E0-\U0001F1FF" # flags (iOS)
103 | "\U00002702-\U000027B0"
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF039
104 | "\U000024C2-\U0001F251"
105 | "\u200d" # zero width joiner
|
= help: Replace with raw string
Unsafe fix
100 100 | "\U0001F300-\U0001F5FF" # symbols & pictographs
101 101 | "\U0001F680-\U0001F6FF" # transport & map symbols
102 102 | "\U0001F1E0-\U0001F1FF" # flags (iOS)
103 |- "\U00002702-\U000027B0"
103 |+ r"\U00002702-\U000027B0"
104 104 | "\U000024C2-\U0001F251"
105 105 | "\u200d" # zero width joiner
106 106 | "\u200c" # zero width non-joiner
RUF039_concat.py:104:5: RUF039 [*] First argument to `re.compile()` is not raw string
|
102 | "\U0001F1E0-\U0001F1FF" # flags (iOS)
103 | "\U00002702-\U000027B0"
104 | "\U000024C2-\U0001F251"
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF039
105 | "\u200d" # zero width joiner
106 | "\u200c" # zero width non-joiner
|
= help: Replace with raw string
Unsafe fix
101 101 | "\U0001F680-\U0001F6FF" # transport & map symbols
102 102 | "\U0001F1E0-\U0001F1FF" # flags (iOS)
103 103 | "\U00002702-\U000027B0"
104 |- "\U000024C2-\U0001F251"
104 |+ r"\U000024C2-\U0001F251"
105 105 | "\u200d" # zero width joiner
106 106 | "\u200c" # zero width non-joiner
107 107 | "\\u200c" # must not be escaped in a raw string
RUF039_concat.py:105:5: RUF039 [*] First argument to `re.compile()` is not raw string
|
103 | "\U00002702-\U000027B0"
104 | "\U000024C2-\U0001F251"
105 | "\u200d" # zero width joiner
| ^^^^^^^^ RUF039
106 | "\u200c" # zero width non-joiner
107 | "\\u200c" # must not be escaped in a raw string
|
= help: Replace with raw string
Unsafe fix
102 102 | "\U0001F1E0-\U0001F1FF" # flags (iOS)
103 103 | "\U00002702-\U000027B0"
104 104 | "\U000024C2-\U0001F251"
105 |- "\u200d" # zero width joiner
105 |+ r"\u200d" # zero width joiner
106 106 | "\u200c" # zero width non-joiner
107 107 | "\\u200c" # must not be escaped in a raw string
108 108 | "]+",
RUF039_concat.py:106:5: RUF039 [*] First argument to `re.compile()` is not raw string
|
104 | "\U000024C2-\U0001F251"
105 | "\u200d" # zero width joiner
106 | "\u200c" # zero width non-joiner
| ^^^^^^^^ RUF039
107 | "\\u200c" # must not be escaped in a raw string
108 | "]+",
|
= help: Replace with raw string
Unsafe fix
103 103 | "\U00002702-\U000027B0"
104 104 | "\U000024C2-\U0001F251"
105 105 | "\u200d" # zero width joiner
106 |- "\u200c" # zero width non-joiner
106 |+ r"\u200c" # zero width non-joiner
107 107 | "\\u200c" # must not be escaped in a raw string
108 108 | "]+",
109 109 | flags=re.UNICODE,
RUF039_concat.py:107:5: RUF039 First argument to `re.compile()` is not raw string
|
105 | "\u200d" # zero width joiner
106 | "\u200c" # zero width non-joiner
107 | "\\u200c" # must not be escaped in a raw string
| ^^^^^^^^^ RUF039
108 | "]+",
109 | flags=re.UNICODE,
|
= help: Replace with raw string
RUF039_concat.py:108:5: RUF039 [*] First argument to `re.compile()` is not raw string
|
106 | "\u200c" # zero width non-joiner
107 | "\\u200c" # must not be escaped in a raw string
108 | "]+",
| ^^^^ RUF039
109 | flags=re.UNICODE,
110 | )
|
= help: Replace with raw string
Safe fix
105 105 | "\u200d" # zero width joiner
106 106 | "\u200c" # zero width non-joiner
107 107 | "\\u200c" # must not be escaped in a raw string
108 |- "]+",
108 |+ r"]+",
109 109 | flags=re.UNICODE,
110 110 | )

View File

@@ -0,0 +1,11 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF039_py_version_sensitive.py:3:12: RUF039 First argument to `re.compile()` is not raw string
|
1 | import re
2 |
3 | re.compile("\N{Partial Differential}") # with unsafe fix if python target is 3.8 or higher, else without fix
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF039
|
= help: Replace with raw string

View File

@@ -0,0 +1,17 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF039_py_version_sensitive.py:3:12: RUF039 [*] First argument to `re.compile()` is not raw string
|
1 | import re
2 |
3 | re.compile("\N{Partial Differential}") # with unsafe fix if python target is 3.8 or higher, else without fix
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF039
|
= help: Replace with raw string
Unsafe fix
1 1 | import re
2 2 |
3 |-re.compile("\N{Partial Differential}") # with unsafe fix if python target is 3.8 or higher, else without fix
3 |+re.compile(r"\N{Partial Differential}") # with unsafe fix if python target is 3.8 or higher, else without fix

View File

@@ -708,23 +708,10 @@ pub struct ComparableTString<'a> {
}
impl<'a> From<&'a ast::TStringValue> for ComparableTString<'a> {
// The approach taken below necessarily deviates from the
// corresponding implementation for [`ast::FStringValue`].
// The reason is that a t-string value is composed of _three_
// non-comparable parts: literals, f-string expressions, and
// t-string interpolations. Since we have merged the AST nodes
// that capture f-string expressions and t-string interpolations
// into the shared [`ast::InterpolatedElement`], we must
// be careful to distinguish between them here.
// We model a [`ComparableTString`] on the actual
// [CPython implementation] of a `string.templatelib.Template` object.
//
// Consequently, we model a [`ComparableTString`] on the actual
// [CPython implementation] of a `string.templatelib.Template` object:
// it is composed of `strings` and `interpolations`. In CPython,
// the `strings` field is a tuple of honest strings (since f-strings
// are evaluated). Our `strings` field will house both f-string
// expressions and string literals.
//
// Finally, as in CPython, we must be careful to ensure that the length
// As in CPython, we must be careful to ensure that the length
// of `strings` is always one more than the length of `interpolations` -
// that way we can recover the original reading order by interleaving
// starting with `strings`. This is how we can tell the
@@ -768,19 +755,6 @@ impl<'a> From<&'a ast::TStringValue> for ComparableTString<'a> {
.push(ComparableInterpolatedStringElement::Literal("".into()));
}
fn push_fstring_expression(&mut self, expression: &'a ast::InterpolatedElement) {
if let Some(ComparableInterpolatedStringElement::Literal(last_literal)) =
self.strings.last()
{
// Recall that we insert empty strings after
// each interpolation. If we encounter an f-string
// expression, we replace the empty string with it.
if last_literal.is_empty() {
self.strings.pop();
}
}
self.strings.push(expression.into());
}
fn push_tstring_interpolation(&mut self, expression: &'a ast::InterpolatedElement) {
self.interpolations.push(expression.into());
self.start_new_literal();
@@ -789,34 +763,13 @@ impl<'a> From<&'a ast::TStringValue> for ComparableTString<'a> {
let mut collector = Collector::default();
for part in value {
match part {
ast::TStringPart::Literal(string_literal) => {
collector.push_literal(&string_literal.value);
for element in value.elements() {
match element {
ast::InterpolatedStringElement::Literal(literal) => {
collector.push_literal(&literal.value);
}
ast::TStringPart::TString(fstring) => {
for element in &fstring.elements {
match element {
ast::InterpolatedStringElement::Literal(literal) => {
collector.push_literal(&literal.value);
}
ast::InterpolatedStringElement::Interpolation(interpolation) => {
collector.push_tstring_interpolation(interpolation);
}
}
}
}
ast::TStringPart::FString(fstring) => {
for element in &fstring.elements {
match element {
ast::InterpolatedStringElement::Literal(literal) => {
collector.push_literal(&literal.value);
}
ast::InterpolatedStringElement::Interpolation(expression) => {
collector.push_fstring_expression(expression);
}
}
}
ast::InterpolatedStringElement::Interpolation(interpolation) => {
collector.push_tstring_interpolation(interpolation);
}
}
}

View File

@@ -320,7 +320,7 @@ pub enum StringLikePartIter<'a> {
String(std::slice::Iter<'a, ast::StringLiteral>),
Bytes(std::slice::Iter<'a, ast::BytesLiteral>),
FString(std::slice::Iter<'a, ast::FStringPart>),
TString(std::slice::Iter<'a, ast::TStringPart>),
TString(std::slice::Iter<'a, ast::TString>),
}
impl<'a> Iterator for StringLikePartIter<'a> {
@@ -339,16 +339,7 @@ impl<'a> Iterator for StringLikePartIter<'a> {
ast::FStringPart::FString(f_string) => StringLikePart::FString(f_string),
}
}
StringLikePartIter::TString(inner) => {
let part = inner.next()?;
match part {
ast::TStringPart::Literal(string_literal) => {
StringLikePart::String(string_literal)
}
ast::TStringPart::TString(t_string) => StringLikePart::TString(t_string),
ast::TStringPart::FString(f_string) => StringLikePart::FString(f_string),
}
}
StringLikePartIter::TString(inner) => StringLikePart::TString(inner.next()?),
};
Some(part)
@@ -378,16 +369,7 @@ impl DoubleEndedIterator for StringLikePartIter<'_> {
ast::FStringPart::FString(f_string) => StringLikePart::FString(f_string),
}
}
StringLikePartIter::TString(inner) => {
let part = inner.next_back()?;
match part {
ast::TStringPart::Literal(string_literal) => {
StringLikePart::String(string_literal)
}
ast::TStringPart::TString(t_string) => StringLikePart::TString(t_string),
ast::TStringPart::FString(f_string) => StringLikePart::FString(f_string),
}
}
StringLikePartIter::TString(inner) => StringLikePart::TString(inner.next_back()?),
};
Some(part)

View File

@@ -1274,6 +1274,7 @@ impl Truthiness {
Self::Unknown
}
}
Expr::TString(_) => Self::Truthy,
Expr::List(ast::ExprList { elts, .. })
| Expr::Set(ast::ExprSet { elts, .. })
| Expr::Tuple(ast::ExprTuple { elts, .. }) => {
@@ -1362,6 +1363,7 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
Expr::EllipsisLiteral(_) => true,
Expr::List(_) => true,
Expr::Tuple(_) => true,
Expr::TString(_) => true,
// These expressions must resolve to the inner expression.
Expr::If(ast::ExprIf { body, orelse, .. }) => inner(body) && inner(orelse),
@@ -1386,7 +1388,6 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
// These literals may or may not be empty.
Expr::FString(f_string) => is_non_empty_f_string(f_string),
// These literals may or may not be empty.
Expr::TString(f_string) => is_non_empty_t_string(f_string),
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(),
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(),
}
@@ -1403,76 +1404,6 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
})
}
/// Returns `true` if the expression definitely resolves to a non-empty string, when used as an
/// f-string expression, or `false` if the expression may resolve to an empty string.
fn is_non_empty_t_string(expr: &ast::ExprTString) -> bool {
fn inner(expr: &Expr) -> bool {
match expr {
// When stringified, these expressions are always non-empty.
Expr::Lambda(_) => true,
Expr::Dict(_) => true,
Expr::Set(_) => true,
Expr::ListComp(_) => true,
Expr::SetComp(_) => true,
Expr::DictComp(_) => true,
Expr::Compare(_) => true,
Expr::NumberLiteral(_) => true,
Expr::BooleanLiteral(_) => true,
Expr::NoneLiteral(_) => true,
Expr::EllipsisLiteral(_) => true,
Expr::List(_) => true,
Expr::Tuple(_) => true,
// These expressions must resolve to the inner expression.
Expr::If(ast::ExprIf { body, orelse, .. }) => inner(body) && inner(orelse),
Expr::Named(ast::ExprNamed { value, .. }) => inner(value),
// These expressions are complex. We can't determine whether they're empty or not.
Expr::BoolOp(ast::ExprBoolOp { .. }) => false,
Expr::BinOp(ast::ExprBinOp { .. }) => false,
Expr::UnaryOp(ast::ExprUnaryOp { .. }) => false,
Expr::Generator(_) => false,
Expr::Await(_) => false,
Expr::Yield(_) => false,
Expr::YieldFrom(_) => false,
Expr::Call(_) => false,
Expr::Attribute(_) => false,
Expr::Subscript(_) => false,
Expr::Starred(_) => false,
Expr::Name(_) => false,
Expr::Slice(_) => false,
Expr::IpyEscapeCommand(_) => false,
// These literals may or may not be empty.
Expr::FString(f_string) => is_non_empty_f_string(f_string),
// These literals may or may not be empty.
Expr::TString(t_string) => is_non_empty_t_string(t_string),
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(),
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(),
}
}
expr.value.iter().any(|part| match part {
ast::TStringPart::Literal(string_literal) => !string_literal.is_empty(),
ast::TStringPart::TString(t_string) => {
t_string.elements.iter().all(|element| match element {
ast::InterpolatedStringElement::Literal(string_literal) => {
!string_literal.is_empty()
}
ast::InterpolatedStringElement::Interpolation(t_string) => {
inner(&t_string.expression)
}
})
}
ast::TStringPart::FString(f_string) => {
f_string.elements.iter().all(|element| match element {
InterpolatedStringElement::Literal(string_literal) => !string_literal.is_empty(),
InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression),
})
}
})
}
/// Returns `true` if the expression definitely resolves to the empty string, when used as an f-string
/// expression.
fn is_empty_f_string(expr: &ast::ExprFString) -> bool {

View File

@@ -29,6 +29,10 @@ impl Name {
Self(compact_str::CompactString::const_new(name))
}
pub fn shrink_to_fit(&mut self) {
self.0.shrink_to_fit();
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}

View File

@@ -171,18 +171,8 @@ impl ast::ExprTString {
node_index: _,
} = self;
for t_string_part in value {
match t_string_part {
ast::TStringPart::Literal(string_literal) => {
visitor.visit_string_literal(string_literal);
}
ast::TStringPart::FString(f_string) => {
visitor.visit_f_string(f_string);
}
ast::TStringPart::TString(t_string) => {
visitor.visit_t_string(t_string);
}
}
for t_string in value {
visitor.visit_t_string(t_string);
}
}
}

View File

@@ -597,8 +597,8 @@ impl ExprTString {
/// otherwise.
pub const fn as_single_part_tstring(&self) -> Option<&TString> {
match &self.value.inner {
TStringValueInner::Single(TStringPart::TString(tstring)) => Some(tstring),
_ => None,
TStringValueInner::Single(tstring) => Some(tstring),
TStringValueInner::Concatenated(_) => None,
}
}
}
@@ -614,7 +614,7 @@ impl TStringValue {
/// Creates a new t-string literal with a single [`TString`] part.
pub fn single(value: TString) -> Self {
Self {
inner: TStringValueInner::Single(TStringPart::TString(value)),
inner: TStringValueInner::Single(value),
}
}
@@ -625,7 +625,7 @@ impl TStringValue {
///
/// Panics if `values` has less than 2 elements.
/// Use [`TStringValue::single`] instead.
pub fn concatenated(values: Vec<TStringPart>) -> Self {
pub fn concatenated(values: Vec<TString>) -> Self {
assert!(
values.len() > 1,
"Use `TStringValue::single` to create single-part t-strings"
@@ -640,78 +640,52 @@ impl TStringValue {
matches!(self.inner, TStringValueInner::Concatenated(_))
}
/// Returns a slice of all the [`TStringPart`]s contained in this value.
pub fn as_slice(&self) -> &[TStringPart] {
/// Returns a slice of all the [`TString`]s contained in this value.
pub fn as_slice(&self) -> &[TString] {
match &self.inner {
TStringValueInner::Single(part) => std::slice::from_ref(part),
TStringValueInner::Concatenated(parts) => parts,
}
}
/// Returns a mutable slice of all the [`TStringPart`]s contained in this value.
fn as_mut_slice(&mut self) -> &mut [TStringPart] {
/// Returns a mutable slice of all the [`TString`]s contained in this value.
fn as_mut_slice(&mut self) -> &mut [TString] {
match &mut self.inner {
TStringValueInner::Single(part) => std::slice::from_mut(part),
TStringValueInner::Concatenated(parts) => parts,
}
}
/// Returns an iterator over all the [`TStringPart`]s contained in this value.
pub fn iter(&self) -> Iter<TStringPart> {
/// Returns an iterator over all the [`TString`]s contained in this value.
pub fn iter(&self) -> Iter<TString> {
self.as_slice().iter()
}
/// Returns an iterator over all the [`TStringPart`]s contained in this value
/// Returns an iterator over all the [`TString`]s contained in this value
/// that allows modification.
pub fn iter_mut(&mut self) -> IterMut<TStringPart> {
pub fn iter_mut(&mut self) -> IterMut<TString> {
self.as_mut_slice().iter_mut()
}
/// Returns an iterator over the [`StringLiteral`] parts contained in this value.
///
/// Note that this doesn't recurse into the t-string parts. For example,
///
/// ```python
/// "foo" t"bar {x}" "baz" t"qux"
/// ```
///
/// Here, the string literal parts returned would be `"foo"` and `"baz"`.
pub fn literals(&self) -> impl Iterator<Item = &StringLiteral> {
self.iter().filter_map(|part| part.as_literal())
}
/// Returns an iterator over the [`TString`] parts contained in this value.
///
/// Note that this doesn't recurse into the t-string parts. For example,
///
/// ```python
/// "foo" t"bar {x}" "baz" t"qux"
/// ```
///
/// Here, the t-string parts returned would be `f"bar {x}"` and `f"qux"`.
pub fn t_strings(&self) -> impl Iterator<Item = &TString> {
self.iter().filter_map(|part| part.as_t_string())
}
/// Returns an iterator over all the [`InterpolatedStringElement`] contained in this value.
///
/// An t-string element is what makes up an [`TString`] i.e., it is either a
/// An interpolated string element is what makes up an [`TString`] i.e., it is either a
/// string literal or an interpolation. In the following example,
///
/// ```python
/// "foo" t"bar {x}" "baz" t"qux"
/// t"foo" t"bar {x}" t"baz" t"qux"
/// ```
///
/// The t-string elements returned would be string literal (`"bar "`),
/// The interpolated string elements returned would be string literal (`"bar "`),
/// interpolation (`x`) and string literal (`"qux"`).
pub fn elements(&self) -> impl Iterator<Item = &InterpolatedStringElement> {
self.t_strings().flat_map(|fstring| fstring.elements.iter())
self.iter().flat_map(|tstring| tstring.elements.iter())
}
}
impl<'a> IntoIterator for &'a TStringValue {
type Item = &'a TStringPart;
type IntoIter = Iter<'a, TStringPart>;
type Item = &'a TString;
type IntoIter = Iter<'a, TString>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
@@ -719,8 +693,8 @@ impl<'a> IntoIterator for &'a TStringValue {
}
impl<'a> IntoIterator for &'a mut TStringValue {
type Item = &'a mut TStringPart;
type IntoIter = IterMut<'a, TStringPart>;
type Item = &'a mut TString;
type IntoIter = IterMut<'a, TString>;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
@@ -731,43 +705,10 @@ impl<'a> IntoIterator for &'a mut TStringValue {
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
enum TStringValueInner {
/// A single t-string i.e., `t"foo"`.
///
/// This is always going to be `TStringPart::TString` variant which is
/// maintained by the `TStringValue::single` constructor.
Single(TStringPart),
Single(TString),
/// An implicitly concatenated t-string i.e., `"foo" t"bar {x}"`.
Concatenated(Vec<TStringPart>),
}
/// An t-string part which is either a string literal, an f-string,
/// or a t-string.
#[derive(Clone, Debug, PartialEq, is_macro::Is)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub enum TStringPart {
Literal(StringLiteral),
FString(FString),
TString(TString),
}
impl TStringPart {
pub fn quote_style(&self) -> Quote {
match self {
Self::Literal(string_literal) => string_literal.flags.quote_style(),
Self::FString(f_string) => f_string.flags.quote_style(),
Self::TString(t_string) => t_string.flags.quote_style(),
}
}
}
impl Ranged for TStringPart {
fn range(&self) -> TextRange {
match self {
TStringPart::Literal(string_literal) => string_literal.range(),
TStringPart::FString(f_string) => f_string.range(),
TStringPart::TString(t_string) => t_string.range(),
}
}
/// An implicitly concatenated t-string i.e., `t"foo" t"bar {x}"`.
Concatenated(Vec<TString>),
}
pub trait StringFlags: Copy {
@@ -1237,6 +1178,12 @@ pub struct TString {
pub flags: TStringFlags,
}
impl TString {
pub fn quote_style(&self) -> Quote {
self.flags.quote_style()
}
}
impl From<TString> for Expr {
fn from(payload: TString) -> Self {
ExprTString {

View File

@@ -7,8 +7,8 @@ use crate::{
self as ast, Alias, AnyParameterRef, Arguments, BoolOp, BytesLiteral, CmpOp, Comprehension,
Decorator, ElifElseClause, ExceptHandler, Expr, ExprContext, FString, FStringPart,
InterpolatedStringElement, Keyword, MatchCase, Operator, Parameter, Parameters, Pattern,
PatternArguments, PatternKeyword, Stmt, StringLiteral, TString, TStringPart, TypeParam,
TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, TypeParams, UnaryOp, WithItem,
PatternArguments, PatternKeyword, Stmt, StringLiteral, TString, TypeParam, TypeParamParamSpec,
TypeParamTypeVar, TypeParamTypeVarTuple, TypeParams, UnaryOp, WithItem,
};
/// A trait for AST visitors. Visits all nodes in the AST recursively in evaluation-order.
@@ -547,14 +547,8 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
}
}
Expr::TString(ast::ExprTString { value, .. }) => {
for part in value {
match part {
TStringPart::Literal(string_literal) => {
visitor.visit_string_literal(string_literal);
}
TStringPart::FString(f_string) => visitor.visit_f_string(f_string),
TStringPart::TString(t_string) => visitor.visit_t_string(t_string),
}
for t_string in value {
visitor.visit_t_string(t_string);
}
}
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {

View File

@@ -533,18 +533,8 @@ pub fn walk_expr<V: Transformer + ?Sized>(visitor: &V, expr: &mut Expr) {
}
}
Expr::TString(ast::ExprTString { value, .. }) => {
for t_string_part in value.iter_mut() {
match t_string_part {
ast::TStringPart::Literal(string_literal) => {
visitor.visit_string_literal(string_literal);
}
ast::TStringPart::FString(f_string) => {
visitor.visit_f_string(f_string);
}
ast::TStringPart::TString(t_string) => {
visitor.visit_t_string(t_string);
}
}
for t_string in value.iter_mut() {
visitor.visit_t_string(t_string);
}
}
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {

View File

@@ -51,28 +51,12 @@ fn concatenated_fstrings_compare_equal() -> Result<(), ParseError> {
#[test]
fn concatenated_tstrings_compare_equal() -> Result<(), ParseError> {
let split_contents = r#"t"{foo!r} this" r"\n raw" t" and {bar!s} that""#;
let split_contents = r#"t"{foo!r} this" rt"\n raw" t" and {bar!s} that""#;
let value_contents = r#"t"{foo!r} this\\n raw and {bar!s} that""#;
assert_comparable(split_contents, value_contents)
}
#[test]
fn concatenated_f_and_t_strings_interwoven_compare_equal() -> Result<(), ParseError> {
let split_contents = r#"f"{foo} this " t"{bar}" "baz""#;
let value_contents = r#"f"{foo}" t" this {bar}" "baz""#;
assert_comparable(split_contents, value_contents)
}
#[test]
fn concatenated_f_and_t_strings_compare_unequal_when_swapped() -> Result<(), ParseError> {
let f_then_t_contents = r#"f"{foo!r} this" r"\n raw" t" and {bar!s} that""#;
let t_then_f_contents = r#"t"{foo!r} this" r"\n raw" f" and {bar!s} that""#;
assert_noncomparable(f_then_t_contents, t_then_f_contents)
}
#[test]
fn t_strings_literal_order_matters_compare_unequal() -> Result<(), ParseError> {
let interp_then_literal_contents = r#"t"{foo}bar""#;
@@ -80,11 +64,3 @@ fn t_strings_literal_order_matters_compare_unequal() -> Result<(), ParseError> {
assert_noncomparable(interp_then_literal_contents, literal_then_interp_contents)
}
#[test]
fn t_strings_empty_concat_equal() -> Result<(), ParseError> {
let empty_literal = r#""" t"hey{foo}""#;
let empty_f_string = r#"f""t"hey{foo}""#;
assert_comparable(empty_literal, empty_f_string)
}

View File

@@ -5,7 +5,8 @@ expression: trace
- ModModule
- StmtExpr
- ExprTString
- StringLiteral
- TString
- InterpolatedStringLiteralElement
- TString
- InterpolatedStringLiteralElement
- InterpolatedElement

View File

@@ -4,7 +4,8 @@ expression: trace
---
- StmtExpr
- ExprTString
- StringLiteral
- TString
- InterpolatedStringLiteralElement
- TString
- InterpolatedStringLiteralElement
- InterpolatedElement

View File

@@ -148,7 +148,7 @@ fn f_strings() {
#[test]
fn t_strings() {
let source = r"'pre' t'foo {bar:.{x}f} baz'";
let source = r"t'pre' t'foo {bar:.{x}f} baz'";
let trace = trace_source_order_visitation(source);

View File

@@ -157,7 +157,7 @@ fn f_strings() {
#[test]
fn t_strings() {
let source = r"'pre' t'foo {bar:.{x}f} baz'";
let source = r"t'pre' t'foo {bar:.{x}f} baz'";
let trace = trace_visitation(source);

View File

@@ -1538,19 +1538,9 @@ impl<'a> Generator<'a> {
fn unparse_t_string_value(&mut self, value: &ast::TStringValue) {
let mut first = true;
for t_string_part in value {
for t_string in value {
self.p_delim(&mut first, " ");
match t_string_part {
ast::TStringPart::Literal(string_literal) => {
self.unparse_string_literal(string_literal);
}
ast::TStringPart::FString(f_string) => {
self.unparse_interpolated_string(&f_string.elements, f_string.flags.into());
}
ast::TStringPart::TString(t_string) => {
self.unparse_interpolated_string(&t_string.elements, t_string.flags.into());
}
}
self.unparse_interpolated_string(&t_string.elements, t_string.flags.into());
}
}

View File

@@ -104,16 +104,13 @@ f"{10 + len('bar')=}" f'{10 + len("bar")=}'
# T-strings
##############################################################################
# Escape `{` and `}` when merging a t-string with a string
"a {not_a_variable}" t"b {10}" "c"
# Join, and break expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more"
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" t"more"
# Join, but don't break the expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more"
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" t"more"
t"test{
expression
@@ -171,22 +168,11 @@ t"test" tR"test"
"single" f""""single"""
"single" t""""single"""
t"single" t""""single"""
b"single" b"""triple"""
##############################################################################
# Don't join t-strings and f-strings
##############################################################################
t"{interp}" f"{expr}"
f"{expr}" t"{interp}"
f"{expr}" "string" t"{interp}"
##############################################################################
# Join strings in with statements
##############################################################################

View File

@@ -345,7 +345,7 @@ a[
b
] = (
t"ccccc{
expression}ccccccccccc" "cccccccccccccccccccccccc" # comment
expression}ccccccccccc" t"cccccccccccccccccccccccc" # comment
)
# Same but starting with a joined string. They should both result in the same formatting.
@@ -361,7 +361,7 @@ a[
aaaaaaa,
b
] = t"ccccc{
expression}ccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment
expression}ccccccccccc" t"ccccccccccccccccccccccccccccccccccccccccccc" # comment
# Split an overlong target, but join the string if it fits
@@ -370,7 +370,7 @@ a[
b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{
expression}ccccccccccc" "cccccccccccccccccccccccccccccc" # comment
expression}ccccccccccc" t"cccccccccccccccccccccccccccccc" # comment
)
# Split both if necessary and keep multiline
@@ -379,66 +379,66 @@ a[
b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{
expression}cccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment
expression}cccccccccccccccccccccccccccccccc" t"ccccccccccccccccccccccccccccccc" # comment
)
# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
}" t"moreeeeeeeeeeeeeeeeeeee" t"test" # comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
}" t"moreeeeeeeeeeeeeeeeeeee" t"test" # comment
)
aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
}" t"moreeeeeeeeeeeeeeeeeeee" t"test" # comment
aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
}" t"moreeeeeeeeeeeeeeeeeeee" t"test" # comment
)
# Don't inline t-strings that contain commented expressions
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
]}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
]}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
# Don't inline t-strings with multiline debug expressions:
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
a=}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
b=}" "moreeeeeeeeeeeeeeeeeetest" # comment
b=}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
=}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
a=}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
=}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
@@ -499,7 +499,7 @@ a = (
)
logger.error(
f"Failed to run task {task} for job"
f"Failed to run task {task} for job"
f"with id {str(job.id)}" # type: ignore[union-attr]
)

View File

@@ -8,21 +8,21 @@ rt"Not-so-tricky \"quote"
# Regression test for tstrings dropping comments
result_f = (
'Traceback (most recent call last):\n'
t'Traceback (most recent call last):\n'
t' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n'
' f()\n'
t' f()\n'
t' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
t' f()\n'
t' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
t' f()\n'
t' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
t' f()\n'
# XXX: The following line changes depending on whether the tests
# are run through the interactive interpreter or with -m
# It also varies depending on the platform (stack size)
# Fortunately, we don't care about exactness here, so we use regex
r' \[Previous line repeated (\d+) more times\]' '\n'
'RecursionError: maximum recursion depth exceeded\n'
rt' \[Previous line repeated (\d+) more times\]' t'\n'
t'RecursionError: maximum recursion depth exceeded\n'
)
@@ -31,7 +31,7 @@ result_f = (
(
t'{1}'
# comment 1
''
t''
)
(
@@ -655,7 +655,7 @@ hello {
# Implicit concatenated t-string containing quotes
_ = (
'This string should change its quotes to double quotes'
t'This string should change its quotes to double quotes'
t'This string uses double quotes in an expression {"it's a quote"}'
t'This t-string does not use any quotes.'
)

View File

@@ -110,16 +110,13 @@ f"{10 + len('bar')=}" f'{10 + len("bar")=}'
# T-strings
##############################################################################
# Escape `{` and `}` when merging a t-string with a string
"a {not_a_variable}" t"b {10}" "c"
# Join, and break expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more"
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" t"more"
# Join, but don't break the expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more"
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" t"more"
t"test{
expression
@@ -177,22 +174,11 @@ t"test" tR"test"
"single" f""""single"""
"single" t""""single"""
t"single" t""""single"""
b"single" b"""triple"""
##############################################################################
# Don't join t-strings and f-strings
##############################################################################
t"{interp}" f"{expr}"
f"{expr}" t"{interp}"
f"{expr}" "string" t"{interp}"
##############################################################################
# Join strings in with statements
##############################################################################
@@ -521,9 +507,6 @@ f"{10 + len('bar')=}" f'{10 + len("bar")=}'
# T-strings
##############################################################################
# Escape `{` and `}` when merging a t-string with a string
t"a {{not_a_variable}}b {10}c"
# Join, and break expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression
@@ -583,22 +566,11 @@ t"test" Rt"test"
"single" f""""single"""
"single" t""""single"""
t"single" t""""single"""
b"single" b"""triple"""
##############################################################################
# Don't join t-strings and f-strings
##############################################################################
t"{interp}" f"{expr}"
f"{expr}" t"{interp}"
f"{expr}" "string" t"{interp}"
##############################################################################
# Join strings in with statements
##############################################################################
@@ -905,7 +877,7 @@ f"aaaaaaaaaaaaaaaa \
```diff
--- Stable
+++ Preview
@@ -302,9 +302,12 @@
@@ -288,9 +288,12 @@
##############################################################################
# Use can_omit_optional_parentheses layout to avoid an instability where the formatter
# picks the can_omit_optional_parentheses layout when the strings are joined.

View File

@@ -351,7 +351,7 @@ a[
b
] = (
t"ccccc{
expression}ccccccccccc" "cccccccccccccccccccccccc" # comment
expression}ccccccccccc" t"cccccccccccccccccccccccc" # comment
)
# Same but starting with a joined string. They should both result in the same formatting.
@@ -367,7 +367,7 @@ a[
aaaaaaa,
b
] = t"ccccc{
expression}ccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment
expression}ccccccccccc" t"ccccccccccccccccccccccccccccccccccccccccccc" # comment
# Split an overlong target, but join the string if it fits
@@ -376,7 +376,7 @@ a[
b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{
expression}ccccccccccc" "cccccccccccccccccccccccccccccc" # comment
expression}ccccccccccc" t"cccccccccccccccccccccccccccccc" # comment
)
# Split both if necessary and keep multiline
@@ -385,66 +385,66 @@ a[
b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{
expression}cccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment
expression}cccccccccccccccccccccccccccccccc" t"ccccccccccccccccccccccccccccccc" # comment
)
# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
}" t"moreeeeeeeeeeeeeeeeeeee" t"test" # comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
}" t"moreeeeeeeeeeeeeeeeeeee" t"test" # comment
)
aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
}" t"moreeeeeeeeeeeeeeeeeeee" t"test" # comment
aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
}" t"moreeeeeeeeeeeeeeeeeeee" t"test" # comment
)
# Don't inline t-strings that contain commented expressions
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
]}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
]}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
# Don't inline t-strings with multiline debug expressions:
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
a=}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
b=}" "moreeeeeeeeeeeeeeeeeetest" # comment
b=}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
=}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
a=}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
=}" t"moreeeeeeeeeeeeeeeeeetest" # comment
)
@@ -505,7 +505,7 @@ a = (
)
logger.error(
f"Failed to run task {task} for job"
f"Failed to run task {task} for job"
f"with id {str(job.id)}" # type: ignore[union-attr]
)
@@ -909,7 +909,7 @@ a[aaaaaaa, b] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccc" # comme
# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit.
a[aaaaaaa, b] = (
t"ccccc{expression}ccccccccccc"
"ccccccccccccccccccccccccccccccccccccccccccc"
t"ccccccccccccccccccccccccccccccccccccccccccc"
) # comment
@@ -925,7 +925,7 @@ a[
aaaaaaa, b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{expression}cccccccccccccccccccccccccccccccc"
"ccccccccccccccccccccccccccccccc"
t"ccccccccccccccccccccccccccccccc"
) # comment
# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma
@@ -935,8 +935,8 @@ aaaaaaaaaaaaaaaaaa = (
a,
]
}"
"moreeeeeeeeeeeeeeeeeeee"
"test"
t"moreeeeeeeeeeeeeeeeeeee"
t"test"
) # comment
aaaaaaaaaaaaaaaaaa = (
@@ -945,8 +945,8 @@ aaaaaaaaaaaaaaaaaa = (
a,
]
}"
"moreeeeeeeeeeeeeeeeeeee"
"test" # comment
t"moreeeeeeeeeeeeeeeeeeee"
t"test" # comment
)
aaaaa[aaaaaaaaaaa] = (
@@ -955,8 +955,8 @@ aaaaa[aaaaaaaaaaa] = (
a,
]
}"
"moreeeeeeeeeeeeeeeeeeee"
"test"
t"moreeeeeeeeeeeeeeeeeeee"
t"test"
) # comment
aaaaa[aaaaaaaaaaa] = (
@@ -965,8 +965,8 @@ aaaaa[aaaaaaaaaaa] = (
a,
]
}"
"moreeeeeeeeeeeeeeeeeeee"
"test" # comment
t"moreeeeeeeeeeeeeeeeeeee"
t"test" # comment
)
# Don't inline t-strings that contain commented expressions
@@ -976,7 +976,7 @@ aaaaaaaaaaaaaaaaaa = (
a # comment
]
}"
"moreeeeeeeeeeeeeeeeeetest" # comment
t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
@@ -985,38 +985,38 @@ aaaaa[aaaaaaaaaaa] = (
a # comment
]
}"
"moreeeeeeeeeeeeeeeeeetest" # comment
t"moreeeeeeeeeeeeeeeeeetest" # comment
)
# Don't inline t-strings with multiline debug expressions:
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
b=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
t"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
t"moreeeeeeeeeeeeeeeeeetest" # comment
)

View File

@@ -14,21 +14,21 @@ rt"Not-so-tricky \"quote"
# Regression test for tstrings dropping comments
result_f = (
'Traceback (most recent call last):\n'
t'Traceback (most recent call last):\n'
t' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n'
' f()\n'
t' f()\n'
t' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
t' f()\n'
t' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
t' f()\n'
t' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
t' f()\n'
# XXX: The following line changes depending on whether the tests
# are run through the interactive interpreter or with -m
# It also varies depending on the platform (stack size)
# Fortunately, we don't care about exactness here, so we use regex
r' \[Previous line repeated (\d+) more times\]' '\n'
'RecursionError: maximum recursion depth exceeded\n'
rt' \[Previous line repeated (\d+) more times\]' t'\n'
t'RecursionError: maximum recursion depth exceeded\n'
)
@@ -37,7 +37,7 @@ result_f = (
(
t'{1}'
# comment 1
''
t''
)
(
@@ -661,7 +661,7 @@ hello {
# Implicit concatenated t-string containing quotes
_ = (
'This string should change its quotes to double quotes'
t'This string should change its quotes to double quotes'
t'This string uses double quotes in an expression {"it's a quote"}'
t'This t-string does not use any quotes.'
)
@@ -761,22 +761,22 @@ rt"Not-so-tricky \"quote"
# Regression test for tstrings dropping comments
result_f = (
"Traceback (most recent call last):\n"
t"Traceback (most recent call last):\n"
t' File "{__file__}", line {lineno_f + 5}, in _check_recursive_traceback_display\n'
" f()\n"
t" f()\n"
t' File "{__file__}", line {lineno_f + 1}, in f\n'
" f()\n"
t" f()\n"
t' File "{__file__}", line {lineno_f + 1}, in f\n'
" f()\n"
t" f()\n"
t' File "{__file__}", line {lineno_f + 1}, in f\n'
" f()\n"
t" f()\n"
# XXX: The following line changes depending on whether the tests
# are run through the interactive interpreter or with -m
# It also varies depending on the platform (stack size)
# Fortunately, we don't care about exactness here, so we use regex
r" \[Previous line repeated (\d+) more times\]"
"\n"
"RecursionError: maximum recursion depth exceeded\n"
rt" \[Previous line repeated (\d+) more times\]"
t"\n"
t"RecursionError: maximum recursion depth exceeded\n"
)
@@ -785,7 +785,7 @@ result_f = (
(
t"{1}"
# comment 1
""
t""
)
(
@@ -1463,7 +1463,7 @@ hello {
# Implicit concatenated t-string containing quotes
_ = (
"This string should change its quotes to double quotes"
t"This string should change its quotes to double quotes"
t"This string uses double quotes in an expression {"it's a quote"}"
t"This t-string does not use any quotes."
)

View File

@@ -3,4 +3,3 @@ t"{hey}"
t'{there}'
t"""what's
happening?"""
"implicitly"t"concatenated"

View File

@@ -3,4 +3,3 @@ t"{hey}"
t'{there}'
t"""what's
happening?"""
"implicitly"t"concatenated"

View File

@@ -17,7 +17,7 @@ t"{ foo = !s }"
t"{ 1, 2 = }"
t'{t"{3.1415=:.1f}":*^20}'
{"foo " t"bar {x + y} " "baz": 10}
{t"foo " t"bar {x + y} " t"baz": 10}
match foo:
case "one":
pass
@@ -44,31 +44,18 @@ t"{x=!a}"
t"{x:.3f!r =}"
t"{x = !r :.3f}"
t"{x:.3f=!r}"
"hello" t"{x}"
t"hello" t"{x}"
t"{x}" t"{y}"
t"{x}" "world"
t"{x}" t"world"
t"Invalid args in command: {command, *args}"
"foo" t"{x}" "bar"
t"foo" t"{x}" t"bar"
(
t"a"
t"b"
"c"
t"c"
rt"d"
fr"e"
tr"e"
)
# With unicode strings
u"foo" t"{bar}" "baz" " some"
"foo" t"{bar}" u"baz" " some"
"foo" t"{bar}" "baz" u" some"
u"foo" t"bar {baz} really" u"bar" "no"
# With f-strings
f"{this}" t"{that}"
t"{this}"f"{that}"
t"{this}" "that" f"{other}"
f"one {this} two" "that" t"three {other} four"
# Nesting
t"{f"{t"{this}"}"}"

View File

@@ -1,4 +1,3 @@
use std::cmp::Ordering;
use std::ops::Deref;
use bitflags::bitflags;
@@ -1256,7 +1255,6 @@ impl<'src> Parser<'src> {
// t'{there}'
// t"""what's
// happening?"""
// "implicitly"t"concatenated"
// test_err template_strings_py313
// # parse_options: {"target-version": "3.13"}
@@ -1264,7 +1262,6 @@ impl<'src> Parser<'src> {
// t'{there}'
// t"""what's
// happening?"""
// "implicitly"t"concatenated"
let string_type = StringType::TString(
self.parse_interpolated_string(InterpolatedStringKind::TString)
.into(),
@@ -1281,7 +1278,7 @@ impl<'src> Parser<'src> {
match strings.len() {
// This is not possible as the function was called by matching against a
// `String` or `FStringStart` token.
// `String`, `FStringStart`, or `TStringStart` token.
0 => unreachable!("Expected to parse at least one string"),
// We need a owned value, hence the `pop` here.
1 => match strings.pop().unwrap() {
@@ -1322,58 +1319,84 @@ impl<'src> Parser<'src> {
) -> Expr {
assert!(strings.len() > 1);
let mut has_tstring = false;
let mut has_fstring = false;
let mut byte_literal_count = 0;
let mut tstring_count = 0;
for string in &strings {
match string {
StringType::FString(_) => has_fstring = true,
StringType::TString(_) => has_tstring = true,
StringType::TString(_) => tstring_count += 1,
StringType::Bytes(_) => byte_literal_count += 1,
StringType::Str(_) => {}
}
}
let has_bytes = byte_literal_count > 0;
let has_tstring = tstring_count > 0;
if has_bytes {
match byte_literal_count.cmp(&strings.len()) {
Ordering::Less => {
// TODO(dhruvmanila): This is not an ideal recovery because the parser
// replaces the byte literals with an invalid string literal node. Any
// downstream tools can extract the raw bytes from the range.
//
// We could convert the node into a string and mark it as invalid
// and would be clever to mark the type which is fewer in quantity.
if byte_literal_count < strings.len() {
// TODO(dhruvmanila): This is not an ideal recovery because the parser
// replaces the byte literals with an invalid string literal node. Any
// downstream tools can extract the raw bytes from the range.
//
// We could convert the node into a string and mark it as invalid
// and would be clever to mark the type which is fewer in quantity.
// test_err mixed_bytes_and_non_bytes_literals
// 'first' b'second'
// f'first' b'second'
// 'first' f'second' b'third'
self.add_error(
ParseErrorType::OtherError(
"Bytes literal cannot be mixed with non-bytes literals".to_string(),
),
range,
);
}
// Only construct a byte expression if all the literals are bytes
// otherwise, we'll try either string, t-string, or f-string. This is to retain
// as much information as possible.
Ordering::Equal => {
let mut values = Vec::with_capacity(strings.len());
for string in strings {
values.push(match string {
StringType::Bytes(value) => value,
_ => unreachable!("Expected `StringType::Bytes`"),
});
}
return Expr::from(ast::ExprBytesLiteral {
value: ast::BytesLiteralValue::concatenated(values),
range,
node_index: AtomicNodeIndex::dummy(),
// test_err mixed_bytes_and_non_bytes_literals
// 'first' b'second'
// f'first' b'second'
// 'first' f'second' b'third'
self.add_error(
ParseErrorType::OtherError(
"Bytes literal cannot be mixed with non-bytes literals".to_string(),
),
range,
);
}
// Only construct a byte expression if all the literals are bytes
// otherwise, we'll try either string, t-string, or f-string. This is to retain
// as much information as possible.
else {
let mut values = Vec::with_capacity(strings.len());
for string in strings {
values.push(match string {
StringType::Bytes(value) => value,
_ => unreachable!("Expected `StringType::Bytes`"),
});
}
Ordering::Greater => unreachable!(),
return Expr::from(ast::ExprBytesLiteral {
value: ast::BytesLiteralValue::concatenated(values),
range,
node_index: AtomicNodeIndex::dummy(),
});
}
}
if has_tstring {
if tstring_count < strings.len() {
self.add_error(
ParseErrorType::OtherError(
"cannot mix t-string literals with string or bytes literals".to_string(),
),
range,
);
}
// Only construct a t-string expression if all the literals are t-strings
// otherwise, we'll try either string or f-string. This is to retain
// as much information as possible.
else {
let mut values = Vec::with_capacity(strings.len());
for string in strings {
values.push(match string {
StringType::TString(value) => value,
_ => unreachable!("Expected `StringType::TString`"),
});
}
return Expr::from(ast::ExprTString {
value: ast::TStringValue::concatenated(values),
range,
node_index: AtomicNodeIndex::dummy(),
});
}
}
@@ -1414,36 +1437,17 @@ impl<'src> Parser<'src> {
});
}
if has_tstring {
let mut parts = Vec::with_capacity(strings.len());
for string in strings {
match string {
StringType::TString(tstring) => parts.push(ast::TStringPart::TString(tstring)),
StringType::FString(fstring) => {
parts.push(ruff_python_ast::TStringPart::FString(fstring));
}
StringType::Str(string) => parts.push(ast::TStringPart::Literal(string)),
StringType::Bytes(bytes) => parts.push(ast::TStringPart::Literal(
ast::StringLiteral::invalid(bytes.range()),
)),
}
}
return Expr::from(ast::ExprTString {
value: ast::TStringValue::concatenated(parts),
range,
node_index: AtomicNodeIndex::dummy(),
});
}
let mut parts = Vec::with_capacity(strings.len());
for string in strings {
match string {
StringType::FString(fstring) => parts.push(ast::FStringPart::FString(fstring)),
StringType::TString(_) => {
unreachable!("expected no tstring parts by this point")
}
StringType::Str(string) => parts.push(ast::FStringPart::Literal(string)),
// Bytes and Template strings are invalid at this point
// and stored as invalid string literal parts in the
// f-string
StringType::TString(tstring) => parts.push(ast::FStringPart::Literal(
ast::StringLiteral::invalid(tstring.range()),
)),
StringType::Bytes(bytes) => parts.push(ast::FStringPart::Literal(
ast::StringLiteral::invalid(bytes.range()),
)),

View File

@@ -13,18 +13,16 @@ expression: suite
range: 0..3,
value: TStringValue {
inner: Single(
TString(
TString {
range: 0..3,
node_index: AtomicNodeIndex(..),
elements: [],
flags: TStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
},
TString {
range: 0..3,
node_index: AtomicNodeIndex(..),
elements: [],
flags: TStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
},
),
},
),
},
},

View File

@@ -1,64 +0,0 @@
---
source: crates/ruff_python_parser/src/string.rs
expression: suite
---
[
Expr(
StmtExpr {
node_index: AtomicNodeIndex(..),
range: 0..18,
value: TString(
ExprTString {
node_index: AtomicNodeIndex(..),
range: 0..18,
value: TStringValue {
inner: Concatenated(
[
FString(
FString {
range: 0..9,
node_index: AtomicNodeIndex(..),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 2..8,
node_index: AtomicNodeIndex(..),
value: "Hello ",
},
),
],
flags: FStringFlags {
quote_style: Single,
prefix: Regular,
triple_quoted: false,
},
},
),
TString(
TString {
range: 10..18,
node_index: AtomicNodeIndex(..),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 12..17,
node_index: AtomicNodeIndex(..),
value: "world",
},
),
],
flags: TStringFlags {
quote_style: Single,
prefix: Regular,
triple_quoted: false,
},
},
),
],
),
},
},
),
},
),
]

View File

@@ -0,0 +1,10 @@
---
source: crates/ruff_python_parser/src/string.rs
expression: suite
---
ParseError {
error: OtherError(
"cannot mix t-string literals with string or bytes literals",
),
location: 0..18,
}

View File

@@ -1,76 +0,0 @@
---
source: crates/ruff_python_parser/src/string.rs
expression: suite
---
[
Expr(
StmtExpr {
node_index: AtomicNodeIndex(..),
range: 0..22,
value: TString(
ExprTString {
node_index: AtomicNodeIndex(..),
range: 0..22,
value: TStringValue {
inner: Concatenated(
[
FString(
FString {
range: 0..9,
node_index: AtomicNodeIndex(..),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 2..8,
node_index: AtomicNodeIndex(..),
value: "Hello ",
},
),
],
flags: FStringFlags {
quote_style: Single,
prefix: Regular,
triple_quoted: false,
},
},
),
TString(
TString {
range: 10..18,
node_index: AtomicNodeIndex(..),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 12..17,
node_index: AtomicNodeIndex(..),
value: "world",
},
),
],
flags: TStringFlags {
quote_style: Single,
prefix: Regular,
triple_quoted: false,
},
},
),
Literal(
StringLiteral {
range: 19..22,
node_index: AtomicNodeIndex(..),
value: "!",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
},
},
),
],
),
},
},
),
},
),
]

View File

@@ -0,0 +1,10 @@
---
source: crates/ruff_python_parser/src/string.rs
expression: suite
---
ParseError {
error: OtherError(
"cannot mix t-string literals with string or bytes literals",
),
location: 0..22,
}

View File

@@ -1,56 +0,0 @@
---
source: crates/ruff_python_parser/src/string.rs
expression: suite
---
[
Expr(
StmtExpr {
node_index: AtomicNodeIndex(..),
range: 0..17,
value: TString(
ExprTString {
node_index: AtomicNodeIndex(..),
range: 0..17,
value: TStringValue {
inner: Concatenated(
[
Literal(
StringLiteral {
range: 0..8,
node_index: AtomicNodeIndex(..),
value: "Hello ",
flags: StringLiteralFlags {
quote_style: Single,
prefix: Empty,
triple_quoted: false,
},
},
),
TString(
TString {
range: 9..17,
node_index: AtomicNodeIndex(..),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 11..16,
node_index: AtomicNodeIndex(..),
value: "world",
},
),
],
flags: TStringFlags {
quote_style: Single,
prefix: Regular,
triple_quoted: false,
},
},
),
],
),
},
},
),
},
),
]

View File

@@ -0,0 +1,10 @@
---
source: crates/ruff_python_parser/src/string.rs
expression: suite
---
ParseError {
error: OtherError(
"cannot mix t-string literals with string or bytes literals",
),
location: 0..17,
}

Some files were not shown because too many files have changed in this diff Show More