Compare commits

..

43 Commits

Author SHA1 Message Date
Micha Reiser
0469eeb357 [ty] Handle overloads in rename 2025-12-11 16:56:46 +01:00
Luca Chiodini
5a9d6a91ea [ty] Uniformly use "not supported" in diagnostics (#21916) 2025-12-11 15:03:55 +00:00
Micha Reiser
c9155d5e72 [ty] Reduce size of ty-ide snapshots (#21915) 2025-12-11 13:36:16 +00:00
Andrew Gallant
8647844572 [ty] Adjust scope completions to use all reachable symbols
Fixes astral-sh/ty#1294
2025-12-11 08:26:15 -05:00
Andrew Gallant
1dcb7f89f1 [ty] Rename all_members_of_scope to all_end_of_scope_members
This reflects more precisely its behavior based on how it uses the
use-def map.
2025-12-11 08:26:15 -05:00
Andrew Gallant
c1c45a6a13 [ty] Remove all_ prefix from some routines on UseDefMap
These routines don't return *all* symbols/members, but rather,
only *for* a particular scope. We do specifically want to add
some routines that return *all* symbols/members, and this naming
scheme made that confusing. It was also inconsistent with other
routines like `all_end_of_scope_symbol_declarations` which *do*
return *all* symbols.
2025-12-11 08:26:15 -05:00
Brent Westbrook
c51727708a Enable --document-private-items for ruff_python_formatter (#21903) 2025-12-11 08:23:10 -05:00
Denys Zhak
27912d46b1 Remove BackwardsTokenizer based parenthesized_range references in ruff_linter (#21836)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-12-11 13:04:57 +01:00
David Peter
71540c03b6 [ty] Revert "Do not infer types for invalid binary expressions in annotations" (#21914)
See discussion here:
https://github.com/astral-sh/ruff/pull/21911#discussion_r2610155157
2025-12-11 11:57:45 +00:00
Micha Reiser
aa27925e87 Skip over trivia tokens after re-lexing (#21895) 2025-12-11 10:45:18 +00:00
Charlie Marsh
5c320990f7 [ty] Avoid inferring types for invalid binary expressions in string annotations (#21911)
## Summary

Closes https://github.com/astral-sh/ty/issues/1847.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-12-11 09:40:19 +01:00
Dhruv Manilawala
24ed28e314 [ty] Improve overload call resolution tracing (#21913)
This PR improves the overload call resolution tracing messages as:
- Use `trace` level instead of `debug` level
- Add a `trace_span` which contains the call arguments and signature
- Remove the signature from individual tracing messages
2025-12-11 12:28:45 +05:30
Carl Meyer
2d0681da08 [ty] fix missing heap_size on Salsa query (#21912) 2025-12-10 18:34:00 -08:00
Ibraheem Ahmed
29bf2cd201 [ty] Support implicit type of cls in signatures (#21771)
## Summary

Extends https://github.com/astral-sh/ruff/pull/20517 to support the
implicit type of `cls` in `@classmethod` signatures. Part of
https://github.com/astral-sh/ty/issues/159.
2025-12-10 16:56:20 -05:00
Jack O'Connor
1b44d7e2a7 [ty] add SyntheticTypedDictType and implement normalized and is_equivalent_to (#21784) 2025-12-10 20:36:36 +00:00
Ibraheem Ahmed
a2fb2ee06c [ty] Fix disjointness checks with type-of @final classes (#21770)
## Summary

We currently perform a subtyping check, similar to what we were doing
for `@final` instances before
https://github.com/astral-sh/ruff/pull/21167, which is incorrect, e.g.
we currently consider `type[X[Any]]` and `type[X[T]]]` disjoint (where
`X` is `@final`).
2025-12-10 15:15:10 -05:00
Douglas Creager
3e00221a6c [ty] Fix negation upper bounds in constraint sets (#21897)
This fixes the logic error that @sharkdp
[found](https://github.com/astral-sh/ruff/pull/21871#discussion_r2605755588)
in the constraint set upper bound normalization logic I introduced in
#21871.

I had originally claimed that `(T ≤ α & ~β)` should simplify into `(T ≤
α) ∧ ¬(T ≤ β)`. But that also suggests that `T ≤ ~β` should simplify to
`¬(T ≤ β)` on its own, and that's not correct.

The correct simplification is that `~α` is an "atomic" type, not an
"intersection" for the purposes of our upper bound simplifcation. So `(T
≤ α & ~β)` should simplify to `(T ≤ α) ∧ (T ≤ ~β)`. That is, break apart
the elements of a (proper) intersection, regardless of whether each
element is negated or not.

This PR fixes the logic, adds a test case, and updates the comments to
be hopefully more clear and accurate.
2025-12-10 15:07:50 -05:00
Ibraheem Ahmed
5dc0079e78 [ty] Fix disjointness checks on @final class instances (#21769)
## Summary

This was left unfinished in
https://github.com/astral-sh/ruff/pull/21167. This is required to fix
our disjointness checks with type-of a final class, which is currently
broken, and blocking https://github.com/astral-sh/ty/issues/159.
2025-12-10 14:17:22 -05:00
Micha Reiser
f7528bd325 [ty] Checking files without extension (#21867) 2025-12-10 16:47:41 +00:00
Avasam
59b92b3522 Document *.pyw is included by default in preview (#21885)
Document `*.pyw` is included by default in preview mode.
Originally requested in https://github.com/astral-sh/ruff/issues/13246
and added in https://github.com/astral-sh/ruff/pull/20458

Co-authored-by: Amethyst Reese <amethyst@n7.gg>
2025-12-10 16:43:55 +00:00
Micha Reiser
9ceec359a0 [ty] Add mypy primer check comparing same revisions (#21864) 2025-12-10 16:37:17 +00:00
Micha Reiser
2dd412c89a Update README to remove production warning (#21899) 2025-12-10 17:25:41 +01:00
Carl Meyer
951766d1fb [ty] default-specialize class-literal types in assignment to generic-alias types (#21883)
Fixes https://github.com/astral-sh/ty/issues/1832, fixes
https://github.com/astral-sh/ty/issues/1513

## Summary

A class object `C` (for which we infer an unspecialized `ClassLiteral`
type) should always be assignable to the type `type[C]` (which is
default-specialized, if `C` is generic). We already implemented this for
most cases, but we missed the case of a generic final type, where we
simplify `type[C]` to the `GenericAlias` type for the default
specialization of `C`. So we also need to implement this assignability
of generic `ClassLiteral` types as-if default-specialized.

## Test Plan

Added mdtests that failed before this PR.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-12-10 17:18:08 +01:00
David Peter
7bf50e70a7 [ty] Generics: Respect typevar bounds when matching against a union (#21893)
## Summary

Respect typevar bounds and constraints when matching against a union.
For example:

```py
def accepts_t_or_int[T_str: str](x: T_str | int) -> T_str:
    raise NotImplementedError

reveal_type(accepts_t_or_int("a"))  # ok, reveals `Literal["a"]`
reveal_type(accepts_t_or_int(1))  # ok, reveals `Unknown`

class Unrelated: ...

# error: [invalid-argument-type] "Argument type `Unrelated` does not
# satisfy upper bound `str` of type variable `T_str`"
accepts_t_or_int(Unrelated())
```

Previously, the last call succeed without any errors. Worse than that,
we also incorrectly solved `T_str = Unrelated`, which often lead to
downstream errors.

closes https://github.com/astral-sh/ty/issues/1837

## Ecosystem impact

Looks good!

* Lots of removed false positives, often because we previously selected
a wrong overload for a generic function (because we didn't respect the
typevar bound in an earlier overload).
* We now understand calls to functions accepting an argument of type
`GenericPath: TypeAlias = AnyStr | PathLike[AnyStr]`. Previously, we
would incorrectly match a `Path` argument against the `AnyStr` typevar
(violating its constraints), but now we match against `PathLike`.

## Performance

Another regression on `colour`. This package uses `numpy` heavily. And
`numpy` is the codebase that originally lead me to this bug. The fix
here allows us to infer more precise `np.array` types in some cases, so
it's reasonable that we just need to perform more work.

The fix here also requires us to look at more union elements when we
would previously short-circuit incorrectly, so some more work needs to
be done in the solver.

## Test Plan

New Markdown tests
2025-12-10 14:58:57 +01:00
Ibraheem Ahmed
ff7086d9ad [ty] Infer type of implicit cls parameter in method bodies (#21685)
## Summary

Extends https://github.com/astral-sh/ruff/pull/20922 to infer
unannotated `cls` parameters as `type[Self]` in method bodies.

Part of https://github.com/astral-sh/ty/issues/159.
2025-12-10 10:31:28 +01:00
Charlie Marsh
d2aabeaaa2 [ty] Respect kw_only from parent class (#21820)
## Summary

Closes https://github.com/astral-sh/ty/issues/1769.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-12-10 10:12:18 +01:00
Dhruv Manilawala
8293afe2ae Remove hack about unknown options warning (#21887)
This hack was introduced to reduce the amount of warnings that users
would get while transitioning to the new settings format
(https://github.com/astral-sh/ruff/pull/19787) but now that we're near
the beta release, it would be good to remove this.
2025-12-10 07:09:31 +00:00
Jack O'Connor
aaadf16b1b [ty] bump dependencies to pull in Salsa support for ordermap (#21854) 2025-12-09 19:08:03 -08:00
Douglas Creager
c343e94ac5 [ty] Simplify union lower bounds and intersection upper bounds in constraint sets (#21871)
In a constraint set, it's not useful for an upper bound to be an
intersection type, or for a lower bound to be a union type. Both of
those can be rewritten as simpler BDDs:

```
T ≤ α & β  ⇒ (T ≤ α) ∧ (T ≤ β)
T ≤ α & ¬β ⇒ (T ≤ α) ∧ ¬(T ≤ β)
α | β ≤ T  ⇒ (α ≤ T) ∧ (β ≤ T)
```

We were seeing performance issues on #21551 when _not_ performing this
simplification. For instance, `pandas` was producing some constraint
sets involving intersections of 8-9 different types. Our sequent map
calculation was timing out calculating all of the different permutations
of those types:

```
t1 & t2 & t3 → t1
t1 & t2 & t3 → t2
t1 & t2 & t3 → t3
t1 & t2 & t3 → t1 & t2
t1 & t2 & t3 → t1 & t3
t1 & t2 & t3 → t2 & t3
```

(and then imagine what that looks like for 9 types instead of 3...)

With this change, all of those permutations are now encoded in the BDD
structure itself, which is very good at simplifying that kind of thing.

Pulling this out of #21551 for separate review.
2025-12-09 19:49:17 -05:00
Douglas Creager
270b8d1d14 [ty] Collapse never paths in constraint set BDDs (#21880)
#21744 fixed some non-determinism in our constraint set implementation
by switching our BDD representation from being "fully reduced" to being
"quasi-reduced". We still deduplicate identical nodes (via salsa
interning), but we removed the logic to prune redundant nodes (one with
identical outgoing true and false edges). This ensures that the BDD
"remembers" all of the individual constraints that it was created with.

However, that comes at the cost of creating larger BDDs, and on #21551
that was causing performance issues. `scikit-learn` was producing a
function signature with dozens of overloads, and we were trying to
create a constraint set that would map a return type typevar to any of
those overload's return types. This created a combinatorial explosion in
the BDD, with by far most of the BDD paths leading to the `never`
terminal.

This change updates the quasi-reduction logic to prune nodes that are
redundant _because both edges lead to the `never` terminal_. In this
case, we don't need to "remember" that constraint, since no assignment
to it can lead to a valid specialization. So we keep the "memory" of our
quasi-reduced structure, while still pruning large unneeded portions of
the BDD structure.

Pulling this out of https://github.com/astral-sh/ruff/pull/21551 for
separate review.
2025-12-09 18:22:54 -05:00
Brent Westbrook
f3714fd3c1 Fix leading comment formatting for lambdas with multiple parameters (#21879)
## Summary

This is a follow-up to #21868. As soon as I started merging #21868 into
#21385, I realized that I had missed a test case with `**kwargs` after
the `*args` parameter. Such a case is supposed to be formatted on one
line like:

```py
# input
(
    lambda
    # comment
    *x,
    **y: x
)

# output
(
    lambda
    # comment
    *x, **y: x
)
```

which you can still see on the
[playground](https://play.ruff.rs/bd88d339-1358-40d2-819f-865bfcb23aef?secondary=Format),
but on `main` after #21868, this was formatted as:

```py
(
    lambda
    # comment
    *x,
    **y: x
)
```

because the leading comment on the first parameter caused the whole
group around the parameters to break.

Instead of making these comments leading comments on the first
parameter, this PR makes them leading comments on the parameters list as
a whole.

## Test Plan

New tests, and I will also try merging this into #21385 _before_ opening
it for review this time.

<hr>

(labeling `internal` since #21868 should not be released before some
kind of fix)
2025-12-09 18:15:12 -05:00
David Peter
a9be810c38 [ty] Type inference for @asynccontextmanager (#21876)
## Summary

This PR adds special handling for `asynccontextmanager` calls as a
temporary solution for https://github.com/astral-sh/ty/issues/1804. We
will be able to remove this soon once we have support for generic
protocols in the solver.

closes https://github.com/astral-sh/ty/issues/1804

## Ecosystem

```diff
+ tests/test_downloadermiddleware.py:305:56: error[invalid-argument-type] Argument to bound method `download` is incorrect: Expected `Spider`, found `Unknown | Spider | None`
+ tests/test_downloadermiddleware.py:305:56: warning[possibly-missing-attribute] Attribute `spider` may be missing on object of type `Crawler | None`
```
These look like true positives

```diff
+ pymongo/asynchronous/database.py:1021:35: error[invalid-assignment] Object of type `(AsyncClientSession & ~AlwaysTruthy & ~AlwaysFalsy) | (_ServerMode & ~AlwaysFalsy) | Unknown | Primary` is not assignable to `_ServerMode | None`
+ pymongo/asynchronous/database.py:1025:17: error[invalid-argument-type] Argument to bound method `_conn_for_reads` is incorrect: Expected `_ServerMode`, found `_ServerMode | None`
```

Known problems or true positives, just caused by the new type for
`session`

```diff
- src/integrations/prefect-sqlalchemy/prefect_sqlalchemy/database.py:269:16: error[invalid-return-type] Return type does not match returned value: expected `Connection | AsyncConnection`, found `_GeneratorContextManager[Unknown, None, None] | _AsyncGeneratorContextManager[Unknown, None] | Connection | AsyncConnection`
+ src/integrations/prefect-sqlalchemy/prefect_sqlalchemy/database.py:269:16: error[invalid-return-type] Return type does not match returned value: expected `Connection | AsyncConnection`, found `_GeneratorContextManager[Unknown, None, None] | _AsyncGeneratorContextManager[AsyncConnection, None] | Connection | AsyncConnection`
```

Just a more concrete type

```diff
- src/prefect/flow_engine.py:1277:24: error[missing-argument] No argument provided for required parameter `cls`
- src/prefect/server/api/server.py:696:49: error[missing-argument] No argument provided for required parameter `cls`
- src/prefect/task_engine.py:1426:24: error[missing-argument] No argument provided for required parameter `cls`
```

Good

## Test Plan

* Adapted and newly added Markdown tests
* Tested on internal codebase
2025-12-09 22:49:00 +01:00
Brent Westbrook
0bec5c0362 Fix comment placement in lambda parameters (#21868)
Summary
--

This PR makes two changes to comment placement in lambda parameters.
First, we
now insert a line break if the first parameter has a leading comment:

```py
# input
(
    lambda
    * # comment 2
    x:
    x
)

# main
(
    lambda # comment 2
    *x: x
)

# this PR
(
    lambda
	# comment 2
    *x: x
)
```

Note the missing space in the output from main. This case is currently
unstable
on main. Also note that the new formatting is more consistent with our
stable
formatting in cases where the lambda has its own dangling comment:

```py
# input
(
    lambda # comment 1
    * # comment 2
    x:
    x
)

# output
(
    lambda  # comment 1
    # comment 2
    *x: x
)
```

and when a parameter without a comment precedes the split `*x`:

```py
# input
(
    lambda y,
    * # comment 2
    x:
    x
)

# output
(
    lambda y,
    # comment 2
    *x: x
)
```

This does change the stable formatting, but I think such cases are rare
(expecting zero hits in the ecosystem report), this fixes an existing
instability, and it should not change any code we've previously
formatted.

Second, this PR modifies the comment placement such that `# comment 2`
in these
outputs is still a leading comment on the parameter. This is also not
the case
on main, where it becomes a [dangling lambda
comment](https://play.ruff.rs/3b29bb7e-70e4-4365-88e0-e60fe1857a35?secondary=Comments).
This doesn't cause any
instability that I'm aware of on main, but it does cause problems when
trying to
adjust the placement of dangling lambda comments in #21385. Changing the
placement in this way should not affect any formatting here.

Test Plan
--

New lambda tests, plus existing tests covering the cases above with
multiple
comments around the parameters (see lambda.py 122-143, and 122-205 or so
more
broadly)

I also checked manually that the comments are now leading on the
parameter:

```shell
❯ cargo run --bin ruff_python_formatter -- --emit stdout --target-version 3.10 --print-comments <<EOF
(
    lambda
        # comment 2
    *x: x
)
EOF
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/ruff_python_formatter --emit stdout --target-version 3.10 --print-comments`
# Comment decoration: Range, Preceding, Following, Enclosing, Comment
21..32, None, Some((Parameters, 37..39)), (ExprLambda, 6..42), "# comment 2"
{
    Node {
        kind: Parameter,
        range: 37..39,
        source: `*x`,
    }: {
        "leading": [
            SourceComment {
                text: "# comment 2",
                position: OwnLine,
                formatted: true,
            },
        ],
        "dangling": [],
        "trailing": [],
    },
}
(
    lambda
    # comment 2
    *x: x
)
```

But I didn't see a great place to put a test like this. Is there
somewhere I can assert this comment placement since it doesn't affect
any formatting yet? Or is it okay to wait until we use this in #21385?
2025-12-09 14:07:48 -05:00
Loïc Riegel
9490fbf1e1 [pylint] Detect subclasses of builtin exceptions (PLW0133) (#21382)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

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

Goal is to detect the useless exception statement not just for builtin
exceptions but also custom (user defined) ones.

## Test Plan

<!-- How was it tested? -->
I added test cases in the rule fixture and updated the insta snapshot.
Note that I first moved up a test case case which was at the bottom to
the correct "violation category".
I wasn't sure if I should create new test cases or just insert inside
those tests. I know that ideally each test case should test only one
thing, but here, duplicating twice 12 test cases seemed very verbose,
and actually less maintainable in the future. The drawback is that the
diff in the snapshot is hard to review, sorry. But you can see that the
snapshot gives 38 diagnostics, which is what we expect.

Alternatively, I also created this file for manual testing.
```py
# tmp/test_error.py

class MyException(Exception):
    ...
class MyBaseException(BaseException):
    ...
class MyValueError(ValueError):
    ...
class MyExceptionCustom(Exception):
    ...
class MyBaseExceptionCustom(BaseException):
    ...
class MyValueErrorCustom(ValueError):
    ...
class MyDeprecationWarning(DeprecationWarning):
    ...
class MyDeprecationWarningCustom(MyDeprecationWarning):
    ...
class MyExceptionGroup(ExceptionGroup):
    ...
class MyExceptionGroupCustom(MyExceptionGroup):
    ...
class MyBaseExceptionGroup(ExceptionGroup):
    ...
class MyBaseExceptionGroupCustom(MyBaseExceptionGroup):
    ...


def foo():
    Exception("...")
    BaseException("...")
    ValueError("...")
    RuntimeError("...")
    DeprecationWarning("...")
    GeneratorExit("...")
    SystemExit("...")
    ExceptionGroup("eg", [ValueError(1), TypeError(2), OSError(3), OSError(4)])
    BaseExceptionGroup("eg", [ValueError(1), TypeError(2), OSError(3), OSError(4)])
    MyException("...")
    MyBaseException("...")
    MyValueError("...")
    MyExceptionCustom("...")
    MyBaseExceptionCustom("...")
    MyValueErrorCustom("...")
    MyDeprecationWarning("...")
    MyDeprecationWarningCustom("...")
    MyExceptionGroup("...")
    MyExceptionGroupCustom("...")
    MyBaseExceptionGroup("...")
    MyBaseExceptionGroupCustom("...")

```

and you can run this to check the PR:
```sh
target/debug/ruff check tmp/test_error.py --select PLW0133 --unsafe-fixes --diff --no-cache --isolated --target-version py310
target/debug/ruff check tmp/test_error.py --select PLW0133 --unsafe-fixes --diff --no-cache --isolated --target-version py314
```
2025-12-09 13:49:55 -05:00
Carl Meyer
8727a7b179 Fix stack overflow with recursive generic protocols (depth limit) (#21858)
## Summary

This fixes https://github.com/astral-sh/ty/issues/1736 where recursive
generic protocols with growing specializations caused a stack overflow.

The issue occurred with protocols like:
```python
class C[T](Protocol):
    a: 'C[set[T]]'
```

When checking `C[set[int]]` against e.g. `C[Unknown]`, member `a`
requires checking `C[set[set[int]]]`, which requires
`C[set[set[set[int]]]]`, etc. Each level has different type
specializations, so the existing cycle detection (using full types as
cache keys) didn't catch the infinite recursion.

This fix adds a simple recursion depth limit (64) to the CycleDetector.
When the depth exceeds the limit, we return the fallback value (assume
compatible) to safely terminate the recursion.

This is a bit of a blunt hammer, but it should be broadly effective to
prevent stack overflow in any nested-relation case, and it's hard to
imagine that non-recursive nested relation comparisons of depth > 64
exist much in the wild.

## Test Plan

Added mdtest.
2025-12-09 09:05:18 -08:00
Amethyst Reese
4e4d018344 New diagnostics for unused range suppressions (#21783)
Issue #3711
2025-12-09 08:30:27 -08:00
Andrew Gallant
a9899af98a [ty] Use default settings in completion tests
This makes it so the test and production environments match.

Ref https://github.com/astral-sh/ruff/pull/21851#discussion_r2601579316
2025-12-09 10:42:46 -05:00
David Peter
aea2bc2308 [ty] Infer type variables within generic unions (#21862)
## Summary

This PR allows our generics solver to find a solution for `T` in cases
like the following:
```py
def extract_t[T](x: P[T] | Q[T]) -> T:
    raise NotImplementedError

reveal_type(extract_t(P[int]()))  # revealed: int
reveal_type(extract_t(Q[str]()))  # revealed: str
```

closes https://github.com/astral-sh/ty/issues/1772
closes https://github.com/astral-sh/ty/issues/1314

## Ecosystem

The impact here looks very good!

It took me a long time to figure this out, but the new diagnostics on
bokeh are actually true positives. I should have tested with another
type-checker immediately, I guess. All other type checkers also emit
errors on these `__init__` calls. MRE
[here](https://play.ty.dev/5c19d260-65e2-4f70-a75e-1a25780843a2) (no
error on main, diagnostic on this branch)

A lot of false positives on home-assistant go away for calls to
functions like
[`async_listen`](180053fe98/homeassistant/core.py (L1581-L1587))
which take a `event_type: EventType[_DataT] | str` parameter. We can now
solve for `_DataT` here, which was previously falling back to its
default value, and then caused problems because it was used as an
argument to an invariant generic class.

## Test Plan

New Markdown tests
2025-12-09 16:22:59 +01:00
Dhruv Manilawala
c35bf8f441 [ty] Fix overload filtering to prefer more "precise" match (#21859)
## Summary

fixes: https://github.com/astral-sh/ty/issues/1809

I took this chance to add some debug level tracing logs for overload
call evaluation similar to Doug's implementation in `constraints.rs`.

## Test Plan

- Add new mdtests
- Tested it against `sqlalchemy.select` in pyx which results in the
correct overload being matched
2025-12-09 20:29:34 +05:30
Andrew Gallant
426125f5c0 [ty] Stabilize auto-import
While still under development, it's far enough along now that we think
it's worth enabling it by default. This should also help give us
feedback for how it behaves.

This PR adds a "completion settings" grouping similar to inlay hints. We
only have an auto-import setting there now, but I expect we'll add more
options to configure completion behavior in the future.

Closes astral-sh/ty#1765
2025-12-09 09:40:38 -05:00
Micha Reiser
a0b18bc153 [ty] Fix reveal-type E2E test (#21865) 2025-12-09 14:08:22 +01:00
Micha Reiser
11901384b4 [ty] Use concise message for LSP clients not supporting related diagnostic information (#21850) 2025-12-09 13:18:30 +01:00
Micha Reiser
dc2f0a86fd Include more details in Tokens 'offset is inside token' panic message (#21860) 2025-12-09 11:12:35 +01:00
242 changed files with 8859 additions and 5261 deletions

View File

@@ -298,7 +298,7 @@ jobs:
# sync, not just public items. Eventually we should do this for all
# crates; for now add crates here as they are warning-clean to prevent
# regression.
- run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db --document-private-items
- run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db -p ruff_python_formatter --document-private-items
env:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
RUSTDOCFLAGS: "-D warnings"

View File

@@ -47,6 +47,7 @@ jobs:
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: "mypy-primer"
workspaces: "ruff"
- name: Install Rust toolchain
@@ -86,6 +87,7 @@ jobs:
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"
shared-key: "mypy-primer"
- name: Install Rust toolchain
run: rustup show
@@ -105,3 +107,54 @@ jobs:
with:
name: mypy_primer_memory_diff
path: mypy_primer_memory.diff
# Runs mypy twice against the same ty version to catch any non-deterministic behavior (ideally).
# The job is disabled for now because there are some non-deterministic diagnostics.
mypy_primer_same_revision:
name: Run mypy_primer on same revision
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
timeout-minutes: 20
# TODO: Enable once we fixed the non-deterministic diagnostics
if: false
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"
shared-key: "mypy-primer"
- name: Install Rust toolchain
run: rustup show
- name: Run determinism check
env:
BASE_REVISION: ${{ github.event.pull_request.head.sha }}
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
CLICOLOR_FORCE: "1"
DIFF_FILE: mypy_primer_determinism.diff
run: |
cd ruff
scripts/mypy_primer.sh
- name: Check for non-determinism
run: |
# Remove ANSI color codes for checking
sed -e 's/\x1b\[[0-9;]*m//g' mypy_primer_determinism.diff > mypy_primer_determinism_clean.diff
# Check if there are any differences (non-determinism)
if [ -s mypy_primer_determinism_clean.diff ]; then
echo "ERROR: Non-deterministic output detected!"
echo "The following differences were found when running ty twice on the same commit:"
cat mypy_primer_determinism_clean.diff
exit 1
else
echo "✓ Output is deterministic"
fi

33
Cargo.lock generated
View File

@@ -1016,7 +1016,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -1108,7 +1108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -1238,9 +1238,9 @@ dependencies = [
[[package]]
name = "get-size-derive2"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff47daa61505c85af126e9dd64af6a342a33dc0cccfe1be74ceadc7d352e6efd"
checksum = "ab21d7bd2c625f2064f04ce54bcb88bc57c45724cde45cba326d784e22d3f71a"
dependencies = [
"attribute-derive",
"quote",
@@ -1249,14 +1249,15 @@ dependencies = [
[[package]]
name = "get-size2"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac7bb8710e1f09672102be7ddf39f764d8440ae74a9f4e30aaa4820dcdffa4af"
checksum = "879272b0de109e2b67b39fcfe3d25fdbba96ac07e44a254f5a0b4d7ff55340cb"
dependencies = [
"compact_str",
"get-size-derive2",
"hashbrown 0.16.1",
"indexmap",
"ordermap",
"smallvec",
]
@@ -1763,7 +1764,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -2233,9 +2234,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordermap"
version = "0.5.12"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b100f7dd605611822d30e182214d3c02fdefce2d801d23993f6b6ba6ca1392af"
checksum = "ed637741ced8fb240855d22a2b4f208dab7a06bcce73380162e5253000c16758"
dependencies = [
"indexmap",
"serde",
@@ -3348,6 +3349,7 @@ dependencies = [
"compact_str",
"get-size2",
"insta",
"itertools 0.14.0",
"memchr",
"ruff_annotate_snippets",
"ruff_python_ast",
@@ -3571,7 +3573,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -3589,7 +3591,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1"
dependencies = [
"boxcar",
"compact_str",
@@ -3600,6 +3602,7 @@ dependencies = [
"indexmap",
"intrusive-collections",
"inventory",
"ordermap",
"parking_lot",
"portable-atomic",
"rustc-hash",
@@ -3613,12 +3616,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1"
[[package]]
name = "salsa-macros"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1"
dependencies = [
"proc-macro2",
"quote",
@@ -3972,7 +3975,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -5026,7 +5029,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]

View File

@@ -88,7 +88,7 @@ etcetera = { version = "0.11.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }
get-size2 = { version = "0.7.0", features = [
get-size2 = { version = "0.7.3", features = [
"derive",
"smallvec",
"hashbrown",
@@ -129,7 +129,7 @@ memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" }
notify = { version = "8.0.0" }
ordermap = { version = "0.5.0" }
ordermap = { version = "1.0.0" }
path-absolutize = { version = "3.1.1" }
path-slash = { version = "0.2.1" }
pathdiff = { version = "0.2.1" }
@@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "55e5e7d32fa3fc189276f35bb04c9438f9aedbd1", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

View File

@@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
13000,
13030,
);
static TANJUN: Benchmark = Benchmark::new(

View File

@@ -2,15 +2,40 @@ from abc import ABC, abstractmethod
from contextlib import suppress
class MyError(Exception):
...
class MySubError(MyError):
...
class MyValueError(ValueError):
...
class MyUserWarning(UserWarning):
...
# Violation test cases with builtin errors: PLW0133
# Test case 1: Useless exception statement
def func():
AssertionError("This is an assertion error") # PLW0133
MyError("This is a custom error") # PLW0133
MySubError("This is a custom error") # PLW0133
MyValueError("This is a custom value error") # PLW0133
# Test case 2: Useless exception statement in try-except block
def func():
try:
Exception("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
except Exception as err:
pass
@@ -19,6 +44,9 @@ def func():
def func():
if True:
RuntimeError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 4: Useless exception statement in class
@@ -26,12 +54,18 @@ def func():
class Class:
def __init__(self):
TypeError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 5: Useless exception statement in function
def func():
def inner():
IndexError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
inner()
@@ -40,6 +74,9 @@ def func():
def func():
while True:
KeyError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 7: Useless exception statement in abstract class
@@ -48,27 +85,58 @@ def func():
@abstractmethod
def method(self):
NotImplementedError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 8: Useless exception statement inside context manager
def func():
with suppress(AttributeError):
with suppress(Exception):
AttributeError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 9: Useless exception statement in parentheses
def func():
(RuntimeError("This is an exception")) # PLW0133
(MyError("This is an exception")) # PLW0133
(MySubError("This is an exception")) # PLW0133
(MyValueError("This is an exception")) # PLW0133
# Test case 10: Useless exception statement in continuation
def func():
x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
x = 1; (MyError("This is an exception")); y = 2 # PLW0133
x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
# Test case 11: Useless warning statement
def func():
UserWarning("This is an assertion error") # PLW0133
UserWarning("This is a user warning") # PLW0133
MyUserWarning("This is a custom user warning") # PLW0133
# Test case 12: Useless exception statement at module level
import builtins
builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
PythonFinalizationError("Added in Python 3.13") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
UserWarning("This is a user warning") # PLW0133
MyUserWarning("This is a custom user warning") # PLW0133
# Non-violation test cases: PLW0133
@@ -119,10 +187,3 @@ def func():
def func():
with suppress(AttributeError):
raise AttributeError("This is an exception") # OK
import builtins
builtins.TypeError("still an exception even though it's an Attribute")
PythonFinalizationError("Added in Python 3.13")

View File

@@ -54,3 +54,35 @@ def f():
# ruff:disable[E741,F841]
I = 1 # noqa: E741,F841
# ruff:enable[E741,F841]
def f():
# TODO: Duplicate codes should be counted as duplicate, not unused
# ruff: disable[F841, F841]
foo = 0
def f():
# Overlapping range suppressions, one should be marked as used,
# and the other should trigger an unused suppression diagnostic
# ruff: disable[F841]
# ruff: disable[F841]
foo = 0
def f():
# Multiple codes but only one is used
# ruff: disable[E741, F401, F841]
foo = 0
def f():
# Multiple codes but only two are used
# ruff: disable[E741, F401, F841]
I = 0
def f():
# Multiple codes but none are used
# ruff: disable[E741, F401, F841]
print("hello")

View File

@@ -437,6 +437,15 @@ impl<'a> Checker<'a> {
}
}
/// Returns the [`Tokens`] for the parsed source file.
///
///
/// Unlike [`Self::tokens`], this method always returns
/// the tokens for the current file, even when within a parsed type annotation.
pub(crate) fn source_tokens(&self) -> &'a Tokens {
self.parsed.tokens()
}
/// The [`Locator`] for the current file, which enables extraction of source code from byte
/// offsets.
pub(crate) const fn locator(&self) -> &'a Locator<'a> {

View File

@@ -119,6 +119,9 @@ pub(crate) fn check_noqa(
}
}
// Diagnostics for unused/invalid range suppressions
suppressions.check_suppressions(context, locator);
// Enforce that the noqa directive was actually used (RUF100), unless RUF100 was itself
// suppressed.
if context.is_rule_enabled(Rule::UnusedNOQA)
@@ -140,8 +143,13 @@ pub(crate) fn check_noqa(
Directive::All(directive) => {
if matches.is_empty() {
let edit = delete_comment(directive.range(), locator);
let mut diagnostic = context
.report_diagnostic(UnusedNOQA { codes: None }, directive.range());
let mut diagnostic = context.report_diagnostic(
UnusedNOQA {
codes: None,
kind: ruff::rules::UnusedNOQAKind::Noqa,
},
directive.range(),
);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary);
diagnostic.set_fix(Fix::safe_edit(edit));
}
@@ -236,6 +244,7 @@ pub(crate) fn check_noqa(
.map(|code| (*code).to_string())
.collect(),
}),
kind: ruff::rules::UnusedNOQAKind::Noqa,
},
directive.range(),
);

View File

@@ -3,14 +3,13 @@
use anyhow::{Context, Result};
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::{self, Tokens, parenthesized_range};
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Parameters, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::textwrap::dedent_to;
use ruff_python_trivia::{
CommentRanges, PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content,
is_python_whitespace,
PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content, is_python_whitespace,
};
use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlines};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@@ -209,7 +208,7 @@ pub(crate) fn remove_argument<T: Ranged>(
arguments: &Arguments,
parentheses: Parentheses,
source: &str,
comment_ranges: &CommentRanges,
tokens: &Tokens,
) -> Result<Edit> {
// Partition into arguments before and after the argument to remove.
let (before, after): (Vec<_>, Vec<_>) = arguments
@@ -224,7 +223,7 @@ pub(crate) fn remove_argument<T: Ranged>(
.context("Unable to find argument")?;
let parenthesized_range =
parenthesized_range(arg.value().into(), arguments.into(), comment_ranges, source)
token::parenthesized_range(arg.value().into(), arguments.into(), tokens)
.unwrap_or(arg.range());
if !after.is_empty() {
@@ -270,25 +269,14 @@ pub(crate) fn remove_argument<T: Ranged>(
///
/// The new argument will be inserted before the first existing keyword argument in `arguments`, if
/// there are any present. Otherwise, the new argument is added to the end of the argument list.
pub(crate) fn add_argument(
argument: &str,
arguments: &Arguments,
comment_ranges: &CommentRanges,
source: &str,
) -> Edit {
pub(crate) fn add_argument(argument: &str, arguments: &Arguments, tokens: &Tokens) -> Edit {
if let Some(ast::Keyword { range, value, .. }) = arguments.keywords.first() {
let keyword = parenthesized_range(value.into(), arguments.into(), comment_ranges, source)
.unwrap_or(*range);
let keyword = parenthesized_range(value.into(), arguments.into(), tokens).unwrap_or(*range);
Edit::insertion(format!("{argument}, "), keyword.start())
} else if let Some(last) = arguments.arguments_source_order().last() {
// Case 1: existing arguments, so append after the last argument.
let last = parenthesized_range(
last.value().into(),
arguments.into(),
comment_ranges,
source,
)
.unwrap_or(last.range());
let last = parenthesized_range(last.value().into(), arguments.into(), tokens)
.unwrap_or(last.range());
Edit::insertion(format!(", {argument}"), last.end())
} else {
// Case 2: no arguments. Add argument, without any trailing comma.

View File

@@ -879,7 +879,7 @@ fn find_noqa_comments<'a>(
exemption: &'a FileExemption,
directives: &'a NoqaDirectives,
noqa_line_for: &NoqaMapping,
suppressions: &Suppressions,
suppressions: &'a Suppressions,
) -> Vec<Option<NoqaComment<'a>>> {
// List of noqa comments, ordered to match up with `messages`
let mut comments_by_line: Vec<Option<NoqaComment<'a>>> = vec![];

View File

@@ -9,6 +9,11 @@ use crate::settings::LinterSettings;
// Rule-specific behavior
// https://github.com/astral-sh/ruff/pull/21382
pub(crate) const fn is_custom_exception_checking_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/15541
pub(crate) const fn is_suspicious_function_reference_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()

View File

@@ -91,8 +91,8 @@ pub(crate) fn fastapi_redundant_response_model(checker: &Checker, function_def:
response_model_arg,
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
checker.comment_ranges(),
checker.source(),
checker.tokens(),
)
.map(Fix::unsafe_edit)
});

View File

@@ -74,12 +74,7 @@ pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
checker
.report_diagnostic(MapWithoutExplicitStrict, call.range())
.set_fix(Fix::applicable_edit(
add_argument(
"strict=False",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
),
add_argument("strict=False", &call.arguments, checker.tokens()),
Applicability::Unsafe,
));
}

View File

@@ -3,7 +3,7 @@ use std::fmt::Write;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, ParameterWithDefault};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::function_type::is_stub;
@@ -166,12 +166,7 @@ fn move_initialization(
return None;
}
let range = match parenthesized_range(
default.into(),
parameter.into(),
checker.comment_ranges(),
checker.source(),
) {
let range = match parenthesized_range(default.into(), parameter.into(), checker.tokens()) {
Some(range) => range,
None => default.range(),
};
@@ -194,13 +189,8 @@ fn move_initialization(
"{} = {}",
parameter.parameter.name(),
locator.slice(
parenthesized_range(
default.into(),
parameter.into(),
checker.comment_ranges(),
checker.source()
)
.unwrap_or(default.range())
parenthesized_range(default.into(), parameter.into(), checker.tokens())
.unwrap_or(default.range())
)
);
} else {

View File

@@ -92,12 +92,7 @@ pub(crate) fn no_explicit_stacklevel(checker: &Checker, call: &ast::ExprCall) {
}
let mut diagnostic = checker.report_diagnostic(NoExplicitStacklevel, call.func.range());
let edit = add_argument(
"stacklevel=2",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
);
let edit = add_argument("stacklevel=2", &call.arguments, checker.tokens());
diagnostic.set_fix(Fix::unsafe_edit(edit));
}

View File

@@ -70,12 +70,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
checker
.report_diagnostic(ZipWithoutExplicitStrict, call.range())
.set_fix(Fix::applicable_edit(
add_argument(
"strict=False",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
),
add_argument("strict=False", &call.arguments, checker.tokens()),
Applicability::Unsafe,
));
}

View File

@@ -2,8 +2,8 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::ExprGenerator;
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::TokenKind;
use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker;
@@ -142,13 +142,9 @@ pub(crate) fn unnecessary_generator_list(checker: &Checker, call: &ast::ExprCall
if *parenthesized {
// The generator's range will include the innermost parentheses, but it could be
// surrounded by additional parentheses.
let range = parenthesized_range(
argument.into(),
(&call.arguments).into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(argument.range());
let range =
parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens())
.unwrap_or(argument.range());
// The generator always parenthesizes the expression; trim the parentheses.
let generator = checker.generator().expr(argument);

View File

@@ -2,8 +2,8 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::ExprGenerator;
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::TokenKind;
use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker;
@@ -147,13 +147,9 @@ pub(crate) fn unnecessary_generator_set(checker: &Checker, call: &ast::ExprCall)
if *parenthesized {
// The generator's range will include the innermost parentheses, but it could be
// surrounded by additional parentheses.
let range = parenthesized_range(
argument.into(),
(&call.arguments).into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(argument.range());
let range =
parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens())
.unwrap_or(argument.range());
// The generator always parenthesizes the expression; trim the parentheses.
let generator = checker.generator().expr(argument);

View File

@@ -1,7 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::TokenKind;
use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker;
@@ -89,13 +89,9 @@ pub(crate) fn unnecessary_list_comprehension_set(checker: &Checker, call: &ast::
// If the list comprehension is parenthesized, remove the parentheses in addition to
// removing the brackets.
let replacement_range = parenthesized_range(
argument.into(),
(&call.arguments).into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or_else(|| argument.range());
let replacement_range =
parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens())
.unwrap_or_else(|| argument.range());
let span = argument.range().add_start(one).sub_end(one);
let replacement =

View File

@@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_python_trivia::is_python_whitespace;
use ruff_source_file::LineRanges;
@@ -88,13 +88,7 @@ pub(crate) fn explicit(checker: &Checker, expr: &Expr) {
checker.report_diagnostic(ExplicitStringConcatenation, expr.range());
let is_parenthesized = |expr: &Expr| {
parenthesized_range(
expr.into(),
bin_op.into(),
checker.comment_ranges(),
checker.source(),
)
.is_some()
parenthesized_range(expr.into(), bin_op.into(), checker.tokens()).is_some()
};
// If either `left` or `right` is parenthesized, generating
// a fix would be too involved. Just report the diagnostic.

View File

@@ -111,7 +111,6 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall
}
let arguments = &call.arguments;
let source = checker.source();
let mut diagnostic = checker.report_diagnostic(ExcInfoOutsideExceptHandler, exc_info.range);
@@ -120,8 +119,8 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall
exc_info,
arguments,
Parentheses::Preserve,
source,
checker.comment_ranges(),
checker.source(),
checker.tokens(),
)?;
Ok(Fix::unsafe_edit(edit))
});

View File

@@ -2,7 +2,7 @@ use itertools::Itertools;
use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_text_size::Ranged;
@@ -129,8 +129,8 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) {
keyword,
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
checker.comment_ranges(),
checker.source(),
checker.tokens(),
)
.map(Fix::safe_edit)
});
@@ -158,8 +158,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) {
parenthesized_range(
value.into(),
dict.into(),
checker.comment_ranges(),
checker.locator().contents(),
checker.tokens()
)
.unwrap_or(value.range())
)

View File

@@ -73,11 +73,11 @@ pub(crate) fn unnecessary_range_start(checker: &Checker, call: &ast::ExprCall) {
let mut diagnostic = checker.report_diagnostic(UnnecessaryRangeStart, start.range());
diagnostic.try_set_fix(|| {
remove_argument(
&start,
start,
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
checker.comment_ranges(),
checker.source(),
checker.tokens(),
)
.map(Fix::safe_edit)
});

View File

@@ -160,20 +160,16 @@ fn generate_fix(
) -> anyhow::Result<Fix> {
let locator = checker.locator();
let source = locator.contents();
let tokens = checker.tokens();
let deletion = remove_argument(
generic_base,
arguments,
Parentheses::Preserve,
source,
checker.comment_ranges(),
tokens,
)?;
let insertion = add_argument(
locator.slice(generic_base),
arguments,
checker.comment_ranges(),
source,
);
let insertion = add_argument(locator.slice(generic_base), arguments, tokens);
Ok(Fix::unsafe_edits(deletion, [insertion]))
}

View File

@@ -5,7 +5,7 @@ use ruff_python_ast::{
helpers::{pep_604_union, typing_optional},
name::Name,
operator_precedence::OperatorPrecedence,
parenthesize::parenthesized_range,
token::{Tokens, parenthesized_range},
};
use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union};
use ruff_text_size::{Ranged, TextRange};
@@ -243,16 +243,12 @@ fn create_fix(
let union_expr = pep_604_union(&[new_literal_expr, none_expr]);
// Check if we need parentheses to preserve operator precedence
let content = if needs_parentheses_for_precedence(
semantic,
literal_expr,
checker.comment_ranges(),
checker.source(),
) {
format!("({})", checker.generator().expr(&union_expr))
} else {
checker.generator().expr(&union_expr)
};
let content =
if needs_parentheses_for_precedence(semantic, literal_expr, checker.tokens()) {
format!("({})", checker.generator().expr(&union_expr))
} else {
checker.generator().expr(&union_expr)
};
let union_edit = Edit::range_replacement(content, literal_expr.range());
Fix::applicable_edit(union_edit, applicability)
@@ -278,8 +274,7 @@ enum UnionKind {
fn needs_parentheses_for_precedence(
semantic: &ruff_python_semantic::SemanticModel,
literal_expr: &Expr,
comment_ranges: &ruff_python_trivia::CommentRanges,
source: &str,
tokens: &Tokens,
) -> bool {
// Get the parent expression to check if we're in a context that needs parentheses
let Some(parent_expr) = semantic.current_expression_parent() else {
@@ -287,14 +282,7 @@ fn needs_parentheses_for_precedence(
};
// Check if the literal expression is already parenthesized
if parenthesized_range(
literal_expr.into(),
parent_expr.into(),
comment_ranges,
source,
)
.is_some()
{
if parenthesized_range(literal_expr.into(), parent_expr.into(), tokens).is_some() {
return false; // Already parenthesized, don't add more
}

View File

@@ -10,7 +10,7 @@ use libcst_native::{
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::Truthiness;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{
self as ast, AnyNodeRef, Arguments, BoolOp, ExceptHandler, Expr, Keyword, Stmt, UnaryOp,
@@ -303,8 +303,7 @@ pub(crate) fn unittest_assertion(
parenthesized_range(
expr.into(),
checker.semantic().current_statement().into(),
checker.comment_ranges(),
checker.locator().contents(),
checker.tokens(),
)
.unwrap_or(expr.range()),
)));

View File

@@ -768,8 +768,8 @@ fn check_fixture_decorator(checker: &Checker, func_name: &str, decorator: &Decor
keyword,
arguments,
edits::Parentheses::Preserve,
checker.locator().contents(),
checker.comment_ranges(),
checker.source(),
checker.tokens(),
)
.map(Fix::unsafe_edit)
});

View File

@@ -2,10 +2,9 @@ use rustc_hash::{FxBuildHasher, FxHashMap};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::{Tokens, parenthesized_range};
use ruff_python_ast::{self as ast, Expr, ExprCall, ExprContext, StringLiteralFlags};
use ruff_python_codegen::Generator;
use ruff_python_trivia::CommentRanges;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -322,18 +321,8 @@ fn elts_to_csv(elts: &[Expr], generator: Generator, flags: StringLiteralFlags) -
/// ```
///
/// This method assumes that the first argument is a string.
fn get_parametrize_name_range(
call: &ExprCall,
expr: &Expr,
comment_ranges: &CommentRanges,
source: &str,
) -> Option<TextRange> {
parenthesized_range(
expr.into(),
(&call.arguments).into(),
comment_ranges,
source,
)
fn get_parametrize_name_range(call: &ExprCall, expr: &Expr, tokens: &Tokens) -> Option<TextRange> {
parenthesized_range(expr.into(), (&call.arguments).into(), tokens)
}
/// PT006
@@ -349,13 +338,8 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr
if names.len() > 1 {
match names_type {
types::ParametrizeNameType::Tuple => {
let name_range = get_parametrize_name_range(
call,
expr,
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(expr.range());
let name_range = get_parametrize_name_range(call, expr, checker.tokens())
.unwrap_or(expr.range());
let mut diagnostic = checker.report_diagnostic(
PytestParametrizeNamesWrongType {
single_argument: false,
@@ -386,13 +370,8 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr
)));
}
types::ParametrizeNameType::List => {
let name_range = get_parametrize_name_range(
call,
expr,
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(expr.range());
let name_range = get_parametrize_name_range(call, expr, checker.tokens())
.unwrap_or(expr.range());
let mut diagnostic = checker.report_diagnostic(
PytestParametrizeNamesWrongType {
single_argument: false,

View File

@@ -10,7 +10,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::helpers::{Truthiness, contains_effect};
use ruff_python_ast::name::Name;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_codegen::Generator;
use ruff_python_semantic::SemanticModel;
@@ -800,14 +800,9 @@ fn is_short_circuit(
edit = Some(get_short_circuit_edit(
value,
TextRange::new(
parenthesized_range(
furthest.into(),
expr.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(furthest.range())
.start(),
parenthesized_range(furthest.into(), expr.into(), checker.tokens())
.unwrap_or(furthest.range())
.start(),
expr.end(),
),
short_circuit_truthiness,
@@ -828,14 +823,9 @@ fn is_short_circuit(
edit = Some(get_short_circuit_edit(
next_value,
TextRange::new(
parenthesized_range(
furthest.into(),
expr.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(furthest.range())
.start(),
parenthesized_range(furthest.into(), expr.into(), checker.tokens())
.unwrap_or(furthest.range())
.start(),
expr.end(),
),
short_circuit_truthiness,

View File

@@ -4,7 +4,7 @@ use ruff_text_size::{Ranged, TextRange};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::{is_const_false, is_const_true};
use ruff_python_ast::name::Name;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use crate::checkers::ast::Checker;
use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation};
@@ -171,13 +171,8 @@ pub(crate) fn if_expr_with_true_false(
checker
.locator()
.slice(
parenthesized_range(
test.into(),
expr.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(test.range()),
parenthesized_range(test.into(), expr.into(), checker.tokens())
.unwrap_or(test.range()),
)
.to_string(),
expr.range(),

View File

@@ -4,10 +4,10 @@ use anyhow::Result;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableStmt;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::stmt_if::{IfElifBranch, if_elif_branches};
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_trivia::{CommentRanges, SimpleTokenKind, SimpleTokenizer};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange};
@@ -99,7 +99,7 @@ pub(crate) fn if_with_same_arms(checker: &Checker, stmt_if: &ast::StmtIf) {
&current_branch,
following_branch,
checker.locator(),
checker.comment_ranges(),
checker.tokens(),
)
});
}
@@ -111,7 +111,7 @@ fn merge_branches(
current_branch: &IfElifBranch,
following_branch: &IfElifBranch,
locator: &Locator,
comment_ranges: &CommentRanges,
tokens: &ruff_python_ast::token::Tokens,
) -> Result<Fix> {
// Identify the colon (`:`) at the end of the current branch's test.
let Some(current_branch_colon) =
@@ -127,12 +127,9 @@ fn merge_branches(
);
// If the following test isn't parenthesized, consider parenthesizing it.
let following_branch_test = if let Some(range) = parenthesized_range(
following_branch.test.into(),
stmt_if.into(),
comment_ranges,
locator.contents(),
) {
let following_branch_test = if let Some(range) =
parenthesized_range(following_branch.test.into(), stmt_if.into(), tokens)
{
Cow::Borrowed(locator.slice(range))
} else if matches!(
following_branch.test,
@@ -153,24 +150,19 @@ fn merge_branches(
//
// For example, if the current test is `x if x else y`, we should parenthesize it to
// `(x if x else y) or ...`.
let parenthesize_edit = if matches!(
current_branch.test,
Expr::Lambda(_) | Expr::Named(_) | Expr::If(_)
) && parenthesized_range(
current_branch.test.into(),
stmt_if.into(),
comment_ranges,
locator.contents(),
)
.is_none()
{
Some(Edit::range_replacement(
format!("({})", locator.slice(current_branch.test)),
current_branch.test.range(),
))
} else {
None
};
let parenthesize_edit =
if matches!(
current_branch.test,
Expr::Lambda(_) | Expr::Named(_) | Expr::If(_)
) && parenthesized_range(current_branch.test.into(), stmt_if.into(), tokens).is_none()
{
Some(Edit::range_replacement(
format!("({})", locator.slice(current_branch.test)),
current_branch.test.range(),
))
} else {
None
};
Ok(Fix::safe_edits(
deletion_edit,

View File

@@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, CmpOp, Comprehension, Expr};
use ruff_python_semantic::analyze::typing;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
@@ -90,20 +90,10 @@ fn key_in_dict(checker: &Checker, left: &Expr, right: &Expr, operator: CmpOp, pa
}
// Extract the exact range of the left and right expressions.
let left_range = parenthesized_range(
left.into(),
parent,
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(left.range());
let right_range = parenthesized_range(
right.into(),
parent,
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(right.range());
let left_range =
parenthesized_range(left.into(), parent, checker.tokens()).unwrap_or(left.range());
let right_range =
parenthesized_range(right.into(), parent, checker.tokens()).unwrap_or(right.range());
let mut diagnostic = checker.report_diagnostic(
InDictKeys {

View File

@@ -11,7 +11,7 @@ use crate::registry::Rule;
use crate::rules::flake8_type_checking::helpers::quote_type_expression;
use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation};
use ruff_python_ast::PythonVersion;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
/// ## What it does
/// Checks if [PEP 613] explicit type aliases contain references to
@@ -295,21 +295,20 @@ pub(crate) fn quoted_type_alias(
let range = annotation_expr.range();
let mut diagnostic = checker.report_diagnostic(QuotedTypeAlias, range);
let fix_string = annotation_expr.value.to_string();
let fix_string = if (fix_string.contains('\n') || fix_string.contains('\r'))
&& parenthesized_range(
// Check for parenthesis outside string ("""...""")
// Check for parentheses outside the string ("""...""")
annotation_expr.into(),
checker.semantic().current_statement().into(),
checker.comment_ranges(),
checker.locator().contents(),
checker.source_tokens(),
)
.is_none()
&& parenthesized_range(
// Check for parenthesis inside string """(...)"""
// Check for parentheses inside the string """(...)"""
expr.into(),
annotation_expr.into(),
checker.comment_ranges(),
checker.locator().contents(),
checker.tokens(),
)
.is_none()
{

View File

@@ -1,10 +1,9 @@
use std::ops::Range;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{Expr, ExprBinOp, ExprCall, Operator};
use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::CommentRanges;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@@ -89,11 +88,7 @@ pub(crate) fn path_constructor_current_directory(
let mut diagnostic = checker.report_diagnostic(PathConstructorCurrentDirectory, arg.range());
match parent_and_next_path_fragment_range(
checker.semantic(),
checker.comment_ranges(),
checker.source(),
) {
match parent_and_next_path_fragment_range(checker.semantic(), checker.tokens()) {
Some((parent_range, next_fragment_range)) => {
let next_fragment_expr = checker.locator().slice(next_fragment_range);
let call_expr = checker.locator().slice(call.range());
@@ -116,7 +111,7 @@ pub(crate) fn path_constructor_current_directory(
arguments,
Parentheses::Preserve,
checker.source(),
checker.comment_ranges(),
checker.tokens(),
)?;
Ok(Fix::applicable_edit(edit, applicability(call.range())))
}),
@@ -125,8 +120,7 @@ pub(crate) fn path_constructor_current_directory(
fn parent_and_next_path_fragment_range(
semantic: &SemanticModel,
comment_ranges: &CommentRanges,
source: &str,
tokens: &ruff_python_ast::token::Tokens,
) -> Option<(TextRange, TextRange)> {
let parent = semantic.current_expression_parent()?;
@@ -142,6 +136,6 @@ fn parent_and_next_path_fragment_range(
Some((
parent.range(),
parenthesized_range(right.into(), parent.into(), comment_ranges, source).unwrap_or(range),
parenthesized_range(right.into(), parent.into(), tokens).unwrap_or(range),
))
}

View File

@@ -1,8 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::{Tokens, parenthesized_range};
use ruff_python_ast::{self as ast, Keyword, Stmt};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged;
use crate::Locator;
@@ -91,7 +90,7 @@ pub(crate) fn inplace_argument(checker: &Checker, call: &ast::ExprCall) {
call,
keyword,
statement,
checker.comment_ranges(),
checker.tokens(),
checker.locator(),
) {
diagnostic.set_fix(fix);
@@ -111,21 +110,16 @@ fn convert_inplace_argument_to_assignment(
call: &ast::ExprCall,
keyword: &Keyword,
statement: &Stmt,
comment_ranges: &CommentRanges,
tokens: &Tokens,
locator: &Locator,
) -> Option<Fix> {
// Add the assignment.
let attr = call.func.as_attribute_expr()?;
let insert_assignment = Edit::insertion(
format!("{name} = ", name = locator.slice(attr.value.range())),
parenthesized_range(
call.into(),
statement.into(),
comment_ranges,
locator.contents(),
)
.unwrap_or(call.range())
.start(),
parenthesized_range(call.into(), statement.into(), tokens)
.unwrap_or(call.range())
.start(),
);
// Remove the `inplace` argument.
@@ -134,7 +128,7 @@ fn convert_inplace_argument_to_assignment(
&call.arguments,
Parentheses::Preserve,
locator.contents(),
comment_ranges,
tokens,
)
.ok()?;

View File

@@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{
self as ast, Expr, ExprEllipsisLiteral, ExprLambda, Identifier, Parameter,
ParameterWithDefault, Parameters, Stmt,
@@ -265,29 +265,19 @@ fn replace_trailing_ellipsis_with_original_expr(
stmt: &Stmt,
checker: &Checker,
) -> String {
let original_expr_range = parenthesized_range(
(&lambda.body).into(),
lambda.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(lambda.body.range());
let original_expr_range =
parenthesized_range((&lambda.body).into(), lambda.into(), checker.tokens())
.unwrap_or(lambda.body.range());
// This prevents the autofix of introducing a syntax error if the lambda's body is an
// expression spanned across multiple lines. To avoid the syntax error we preserve
// the parenthesis around the body.
let original_expr_in_source = if parenthesized_range(
lambda.into(),
stmt.into(),
checker.comment_ranges(),
checker.source(),
)
.is_some()
{
format!("({})", checker.locator().slice(original_expr_range))
} else {
checker.locator().slice(original_expr_range).to_string()
};
let original_expr_in_source =
if parenthesized_range(lambda.into(), stmt.into(), checker.tokens()).is_some() {
format!("({})", checker.locator().slice(original_expr_range))
} else {
checker.locator().slice(original_expr_range).to_string()
};
let placeholder_ellipsis_start = generated.rfind("...").unwrap();
let placeholder_ellipsis_end = placeholder_ellipsis_start + "...".len();

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::{Tokens, parenthesized_range};
use rustc_hash::FxHashMap;
use ruff_macros::{ViolationMetadata, derive_message_formats};
@@ -179,15 +179,14 @@ fn is_redundant_boolean_comparison(op: CmpOp, comparator: &Expr) -> Option<bool>
fn generate_redundant_comparison(
compare: &ast::ExprCompare,
comment_ranges: &ruff_python_trivia::CommentRanges,
tokens: &Tokens,
source: &str,
comparator: &Expr,
kind: bool,
needs_wrap: bool,
) -> String {
let comparator_range =
parenthesized_range(comparator.into(), compare.into(), comment_ranges, source)
.unwrap_or(comparator.range());
let comparator_range = parenthesized_range(comparator.into(), compare.into(), tokens)
.unwrap_or(comparator.range());
let comparator_str = &source[comparator_range];
@@ -379,7 +378,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
.copied()
.collect::<Vec<_>>();
let comment_ranges = checker.comment_ranges();
let tokens = checker.tokens();
let source = checker.source();
let content = match (&*compare.ops, &*compare.comparators) {
@@ -387,18 +386,13 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
if let Some(kind) = is_redundant_boolean_comparison(*op, &compare.left) {
let needs_wrap = compare.left.range().start() != compare.range().start();
generate_redundant_comparison(
compare,
comment_ranges,
source,
comparator,
kind,
needs_wrap,
compare, tokens, source, comparator, kind, needs_wrap,
)
} else if let Some(kind) = is_redundant_boolean_comparison(*op, comparator) {
let needs_wrap = comparator.range().end() != compare.range().end();
generate_redundant_comparison(
compare,
comment_ranges,
tokens,
source,
&compare.left,
kind,
@@ -410,7 +404,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
&ops,
&compare.comparators,
compare.into(),
comment_ranges,
tokens,
source,
)
}
@@ -420,7 +414,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
&ops,
&compare.comparators,
compare.into(),
comment_ranges,
tokens,
source,
),
};

View File

@@ -107,7 +107,7 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) {
&[CmpOp::NotIn],
comparators,
unary_op.into(),
checker.comment_ranges(),
checker.tokens(),
checker.source(),
),
unary_op.range(),
@@ -127,7 +127,7 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) {
&[CmpOp::IsNot],
comparators,
unary_op.into(),
checker.comment_ranges(),
checker.tokens(),
checker.source(),
),
unary_op.range(),

View File

@@ -3,7 +3,7 @@ use std::collections::hash_map::Entry;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::{ComparableExpr, HashableExpr};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
@@ -193,16 +193,14 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) {
parenthesized_range(
dict.value(i - 1).into(),
dict.into(),
checker.comment_ranges(),
checker.locator().contents(),
checker.tokens(),
)
.unwrap_or_else(|| dict.value(i - 1).range())
.end(),
parenthesized_range(
dict.value(i).into(),
dict.into(),
checker.comment_ranges(),
checker.locator().contents(),
checker.tokens(),
)
.unwrap_or_else(|| dict.value(i).range())
.end(),
@@ -224,16 +222,14 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) {
parenthesized_range(
dict.value(i - 1).into(),
dict.into(),
checker.comment_ranges(),
checker.locator().contents(),
checker.tokens(),
)
.unwrap_or_else(|| dict.value(i - 1).range())
.end(),
parenthesized_range(
dict.value(i).into(),
dict.into(),
checker.comment_ranges(),
checker.locator().contents(),
checker.tokens(),
)
.unwrap_or_else(|| dict.value(i).range())
.end(),

View File

@@ -2,7 +2,7 @@ use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_ast::{self as ast, Stmt};
use ruff_python_semantic::Binding;
@@ -172,14 +172,10 @@ fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option<Fix> {
{
// If the expression is complex (`x = foo()`), remove the assignment,
// but preserve the right-hand side.
let start = parenthesized_range(
target.into(),
statement.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(target.range())
.start();
let start =
parenthesized_range(target.into(), statement.into(), checker.tokens())
.unwrap_or(target.range())
.start();
let end = match_token_after(checker.tokens(), target.end(), |token| {
token == TokenKind::Equal
})?

View File

@@ -16,10 +16,10 @@ mod tests {
use crate::registry::Rule;
use crate::rules::{flake8_tidy_imports, pylint};
use crate::assert_diagnostics;
use crate::settings::LinterSettings;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_diagnostics, assert_diagnostics_diff};
#[test_case(Rule::SingledispatchMethod, Path::new("singledispatch_method.py"))]
#[test_case(
@@ -253,6 +253,32 @@ mod tests {
Ok(())
}
#[test_case(
Rule::UselessExceptionStatement,
Path::new("useless_exception_statement.py")
)]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
assert_diagnostics_diff!(
snapshot,
Path::new("pylint").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Disabled,
..LinterSettings::for_rule(rule_code)
},
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
}
);
Ok(())
}
#[test]
fn continue_in_finally() -> Result<()> {
let diagnostics = test_path(

View File

@@ -2,7 +2,7 @@ use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{
BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare,
parenthesize::{parentheses_iterator, parenthesized_range},
token::{parentheses_iterator, parenthesized_range},
};
use ruff_text_size::{Ranged, TextRange};
@@ -62,7 +62,7 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB
}
let locator = checker.locator();
let comment_ranges = checker.comment_ranges();
let tokens = checker.tokens();
// retrieve all compare expressions from boolean expression
let compare_expressions = expr_bool_op
@@ -89,40 +89,22 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB
continue;
}
let left_paren_count = parentheses_iterator(
left_compare.into(),
Some(expr_bool_op.into()),
comment_ranges,
locator.contents(),
)
.count();
let left_paren_count =
parentheses_iterator(left_compare.into(), Some(expr_bool_op.into()), tokens).count();
let right_paren_count = parentheses_iterator(
right_compare.into(),
Some(expr_bool_op.into()),
comment_ranges,
locator.contents(),
)
.count();
let right_paren_count =
parentheses_iterator(right_compare.into(), Some(expr_bool_op.into()), tokens).count();
// Create the edit that removes the comparison operator
// In `a<(b) and ((b))<c`, we need to handle the
// parentheses when specifying the fix range.
let left_compare_right_range = parenthesized_range(
left_compare_right.into(),
left_compare.into(),
comment_ranges,
locator.contents(),
)
.unwrap_or(left_compare_right.range());
let right_compare_left_range = parenthesized_range(
right_compare_left.into(),
right_compare.into(),
comment_ranges,
locator.contents(),
)
.unwrap_or(right_compare_left.range());
let left_compare_right_range =
parenthesized_range(left_compare_right.into(), left_compare.into(), tokens)
.unwrap_or(left_compare_right.range());
let right_compare_left_range =
parenthesized_range(right_compare_left.into(), right_compare.into(), tokens)
.unwrap_or(right_compare_left.range());
let edit = Edit::range_replacement(
locator.slice(left_compare_right_range).to_string(),
TextRange::new(

View File

@@ -99,7 +99,7 @@ pub(crate) fn duplicate_bases(checker: &Checker, name: &str, arguments: Option<&
arguments,
Parentheses::Remove,
checker.locator().contents(),
checker.comment_ranges(),
checker.tokens(),
)
.map(|edit| {
Fix::applicable_edit(

View File

@@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, CmpOp, Stmt};
use ruff_text_size::Ranged;
@@ -166,13 +166,8 @@ pub(crate) fn if_stmt_min_max(checker: &Checker, stmt_if: &ast::StmtIf) {
let replacement = format!(
"{} = {min_max}({}, {})",
checker.locator().slice(
parenthesized_range(
body_target.into(),
body.into(),
checker.comment_ranges(),
checker.locator().contents()
)
.unwrap_or(body_target.range())
parenthesized_range(body_target.into(), body.into(), checker.tokens())
.unwrap_or(body_target.range())
),
checker.locator().slice(arg1),
checker.locator().slice(arg2),

View File

@@ -174,12 +174,8 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
SliceBoundary::Last => "rsplit",
};
let maxsplit_argument_edit = fix::edits::add_argument(
"maxsplit=1",
arguments,
checker.comment_ranges(),
checker.locator().contents(),
);
let maxsplit_argument_edit =
fix::edits::add_argument("maxsplit=1", arguments, checker.tokens());
// 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 {

View File

@@ -2,7 +2,7 @@ use ast::Expr;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{ExprBinOp, ExprRef, Operator};
use ruff_text_size::{Ranged, TextRange};
@@ -150,12 +150,10 @@ fn augmented_assignment(
let right_operand_ref = ExprRef::from(right_operand);
let parent = original_expr.into();
let comment_ranges = checker.comment_ranges();
let source = checker.source();
let tokens = checker.tokens();
let right_operand_range =
parenthesized_range(right_operand_ref, parent, comment_ranges, source)
.unwrap_or(right_operand.range());
parenthesized_range(right_operand_ref, parent, tokens).unwrap_or(right_operand.range());
let right_operand_expr = locator.slice(right_operand_range);
let target_expr = locator.slice(target);

View File

@@ -75,12 +75,7 @@ pub(crate) fn subprocess_run_without_check(checker: &Checker, call: &ast::ExprCa
let mut diagnostic =
checker.report_diagnostic(SubprocessRunWithoutCheck, call.func.range());
diagnostic.set_fix(Fix::applicable_edit(
add_argument(
"check=False",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
),
add_argument("check=False", &call.arguments, checker.tokens()),
// If the function call contains `**kwargs`, mark the fix as unsafe.
if call
.arguments

View File

@@ -1,8 +1,7 @@
use std::fmt::{Display, Formatter};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Expr, name::QualifiedName};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::typing;
use ruff_text_size::{Ranged, TextRange};
@@ -193,8 +192,7 @@ fn generate_keyword_fix(checker: &Checker, call: &ast::ExprCall) -> Fix {
}))
),
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
checker.tokens(),
))
}

View File

@@ -1,10 +1,11 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::{SemanticModel, analyze};
use ruff_python_stdlib::builtins;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::is_custom_exception_checking_enabled;
use crate::{Edit, Fix, FixAvailability, Violation};
use ruff_python_ast::PythonVersion;
@@ -20,6 +21,9 @@ use ruff_python_ast::PythonVersion;
/// This rule only detects built-in exceptions, like `ValueError`, and does
/// not catch user-defined exceptions.
///
/// In [preview], this rule will also detect user-defined exceptions, but only
/// the ones defined in the file being checked.
///
/// ## Example
/// ```python
/// ValueError("...")
@@ -32,7 +36,8 @@ use ruff_python_ast::PythonVersion;
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as converting a useless exception
/// statement to a `raise` statement will change the program's behavior.
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "0.5.0")]
pub(crate) struct UselessExceptionStatement;
@@ -56,7 +61,10 @@ pub(crate) fn useless_exception_statement(checker: &Checker, expr: &ast::StmtExp
return;
};
if is_builtin_exception(func, checker.semantic(), checker.target_version()) {
if is_builtin_exception(func, checker.semantic(), checker.target_version())
|| (is_custom_exception_checking_enabled(checker.settings())
&& is_custom_exception(func, checker.semantic(), checker.target_version()))
{
let mut diagnostic = checker.report_diagnostic(UselessExceptionStatement, expr.range());
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
"raise ".to_string(),
@@ -78,3 +86,34 @@ fn is_builtin_exception(
if builtins::is_exception(name, target_version.minor))
})
}
/// Returns `true` if the given expression is a custom exception.
fn is_custom_exception(
expr: &Expr,
semantic: &SemanticModel,
target_version: PythonVersion,
) -> bool {
let Some(qualified_name) = semantic.resolve_qualified_name(expr) else {
return false;
};
let Some(symbol) = qualified_name.segments().last() else {
return false;
};
let Some(binding_id) = semantic.lookup_symbol(symbol) else {
return false;
};
let binding = semantic.binding(binding_id);
let Some(source) = binding.source else {
return false;
};
let statement = semantic.statement(source);
if let ast::Stmt::ClassDef(class_def) = statement {
return analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
if let ["" | "builtins", name] = qualified_name.segments() {
return builtins::is_exception(name, target_version.minor);
}
false
});
}
false
}

View File

@@ -2,250 +2,294 @@
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:7:5
|
5 | # Test case 1: Useless exception statement
6 | def func():
7 | AssertionError("This is an assertion error") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
4 |
5 | # Test case 1: Useless exception statement
6 | def func():
- AssertionError("This is an assertion error") # PLW0133
7 + raise AssertionError("This is an assertion error") # PLW0133
8 |
9 |
10 | # Test case 2: Useless exception statement in try-except block
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:13:9
--> useless_exception_statement.py:26:5
|
11 | def func():
12 | try:
13 | Exception("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14 | except Exception as err:
15 | pass
|
help: Add `raise` keyword
10 | # Test case 2: Useless exception statement in try-except block
11 | def func():
12 | try:
- Exception("This is an exception") # PLW0133
13 + raise Exception("This is an exception") # PLW0133
14 | except Exception as err:
15 | pass
16 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:21:9
|
19 | def func():
20 | if True:
21 | RuntimeError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
18 | # Test case 3: Useless exception statement in if statement
19 | def func():
20 | if True:
- RuntimeError("This is an exception") # PLW0133
21 + raise RuntimeError("This is an exception") # PLW0133
22 |
23 |
24 | # Test case 4: Useless exception statement in class
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:28:13
|
26 | class Class:
27 | def __init__(self):
28 | TypeError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
24 | # Test case 1: Useless exception statement
25 | def func():
26 | class Class:
27 | def __init__(self):
26 | AssertionError("This is an assertion error") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
27 | MyError("This is a custom error") # PLW0133
28 | MySubError("This is a custom error") # PLW0133
|
help: Add `raise` keyword
23 |
24 | # Test case 1: Useless exception statement
25 | def func():
- AssertionError("This is an assertion error") # PLW0133
26 + raise AssertionError("This is an assertion error") # PLW0133
27 | MyError("This is a custom error") # PLW0133
28 | MySubError("This is a custom error") # PLW0133
29 | MyValueError("This is a custom value error") # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:35:9
|
33 | def func():
34 | try:
35 | Exception("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
36 | MyError("This is an exception") # PLW0133
37 | MySubError("This is an exception") # PLW0133
|
help: Add `raise` keyword
32 | # Test case 2: Useless exception statement in try-except block
33 | def func():
34 | try:
- Exception("This is an exception") # PLW0133
35 + raise Exception("This is an exception") # PLW0133
36 | MyError("This is an exception") # PLW0133
37 | MySubError("This is an exception") # PLW0133
38 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:46:9
|
44 | def func():
45 | if True:
46 | RuntimeError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
47 | MyError("This is an exception") # PLW0133
48 | MySubError("This is an exception") # PLW0133
|
help: Add `raise` keyword
43 | # Test case 3: Useless exception statement in if statement
44 | def func():
45 | if True:
- RuntimeError("This is an exception") # PLW0133
46 + raise RuntimeError("This is an exception") # PLW0133
47 | MyError("This is an exception") # PLW0133
48 | MySubError("This is an exception") # PLW0133
49 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:56:13
|
54 | class Class:
55 | def __init__(self):
56 | TypeError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
57 | MyError("This is an exception") # PLW0133
58 | MySubError("This is an exception") # PLW0133
|
help: Add `raise` keyword
53 | def func():
54 | class Class:
55 | def __init__(self):
- TypeError("This is an exception") # PLW0133
28 + raise TypeError("This is an exception") # PLW0133
29 |
30 |
31 | # Test case 5: Useless exception statement in function
56 + raise TypeError("This is an exception") # PLW0133
57 | MyError("This is an exception") # PLW0133
58 | MySubError("This is an exception") # PLW0133
59 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:34:9
--> useless_exception_statement.py:65:9
|
32 | def func():
33 | def inner():
34 | IndexError("This is an exception") # PLW0133
63 | def func():
64 | def inner():
65 | IndexError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35 |
36 | inner()
66 | MyError("This is an exception") # PLW0133
67 | MySubError("This is an exception") # PLW0133
|
help: Add `raise` keyword
31 | # Test case 5: Useless exception statement in function
32 | def func():
33 | def inner():
62 | # Test case 5: Useless exception statement in function
63 | def func():
64 | def inner():
- IndexError("This is an exception") # PLW0133
34 + raise IndexError("This is an exception") # PLW0133
35 |
36 | inner()
37 |
65 + raise IndexError("This is an exception") # PLW0133
66 | MyError("This is an exception") # PLW0133
67 | MySubError("This is an exception") # PLW0133
68 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:42:9
--> useless_exception_statement.py:76:9
|
40 | def func():
41 | while True:
42 | KeyError("This is an exception") # PLW0133
74 | def func():
75 | while True:
76 | KeyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
77 | MyError("This is an exception") # PLW0133
78 | MySubError("This is an exception") # PLW0133
|
help: Add `raise` keyword
39 | # Test case 6: Useless exception statement in while loop
40 | def func():
41 | while True:
73 | # Test case 6: Useless exception statement in while loop
74 | def func():
75 | while True:
- KeyError("This is an exception") # PLW0133
42 + raise KeyError("This is an exception") # PLW0133
43 |
44 |
45 | # Test case 7: Useless exception statement in abstract class
76 + raise KeyError("This is an exception") # PLW0133
77 | MyError("This is an exception") # PLW0133
78 | MySubError("This is an exception") # PLW0133
79 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:50:13
--> useless_exception_statement.py:87:13
|
48 | @abstractmethod
49 | def method(self):
50 | NotImplementedError("This is an exception") # PLW0133
85 | @abstractmethod
86 | def method(self):
87 | NotImplementedError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
88 | MyError("This is an exception") # PLW0133
89 | MySubError("This is an exception") # PLW0133
|
help: Add `raise` keyword
47 | class Class(ABC):
48 | @abstractmethod
49 | def method(self):
84 | class Class(ABC):
85 | @abstractmethod
86 | def method(self):
- NotImplementedError("This is an exception") # PLW0133
50 + raise NotImplementedError("This is an exception") # PLW0133
51 |
52 |
53 | # Test case 8: Useless exception statement inside context manager
87 + raise NotImplementedError("This is an exception") # PLW0133
88 | MyError("This is an exception") # PLW0133
89 | MySubError("This is an exception") # PLW0133
90 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:56:9
--> useless_exception_statement.py:96:9
|
54 | def func():
55 | with suppress(AttributeError):
56 | AttributeError("This is an exception") # PLW0133
94 | def func():
95 | with suppress(Exception):
96 | AttributeError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
97 | MyError("This is an exception") # PLW0133
98 | MySubError("This is an exception") # PLW0133
|
help: Add `raise` keyword
53 | # Test case 8: Useless exception statement inside context manager
54 | def func():
55 | with suppress(AttributeError):
93 | # Test case 8: Useless exception statement inside context manager
94 | def func():
95 | with suppress(Exception):
- AttributeError("This is an exception") # PLW0133
56 + raise AttributeError("This is an exception") # PLW0133
57 |
58 |
59 | # Test case 9: Useless exception statement in parentheses
96 + raise AttributeError("This is an exception") # PLW0133
97 | MyError("This is an exception") # PLW0133
98 | MySubError("This is an exception") # PLW0133
99 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:61:5
|
59 | # Test case 9: Useless exception statement in parentheses
60 | def func():
61 | (RuntimeError("This is an exception")) # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
58 |
59 | # Test case 9: Useless exception statement in parentheses
60 | def func():
- (RuntimeError("This is an exception")) # PLW0133
61 + raise (RuntimeError("This is an exception")) # PLW0133
62 |
63 |
64 | # Test case 10: Useless exception statement in continuation
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:66:12
|
64 | # Test case 10: Useless exception statement in continuation
65 | def func():
66 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
63 |
64 | # Test case 10: Useless exception statement in continuation
65 | def func():
- x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
66 + x = 1; raise (RuntimeError("This is an exception")); y = 2 # PLW0133
67 |
68 |
69 | # Test case 11: Useless warning statement
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:71:5
|
69 | # Test case 11: Useless warning statement
70 | def func():
71 | UserWarning("This is an assertion error") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
68 |
69 | # Test case 11: Useless warning statement
70 | def func():
- UserWarning("This is an assertion error") # PLW0133
71 + raise UserWarning("This is an assertion error") # PLW0133
72 |
73 |
74 | # Non-violation test cases: PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:126:1
--> useless_exception_statement.py:104:5
|
124 | import builtins
125 |
126 | builtins.TypeError("still an exception even though it's an Attribute")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
127 |
128 | PythonFinalizationError("Added in Python 3.13")
102 | # Test case 9: Useless exception statement in parentheses
103 | def func():
104 | (RuntimeError("This is an exception")) # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
105 | (MyError("This is an exception")) # PLW0133
106 | (MySubError("This is an exception")) # PLW0133
|
help: Add `raise` keyword
101 |
102 | # Test case 9: Useless exception statement in parentheses
103 | def func():
- (RuntimeError("This is an exception")) # PLW0133
104 + raise (RuntimeError("This is an exception")) # PLW0133
105 | (MyError("This is an exception")) # PLW0133
106 | (MySubError("This is an exception")) # PLW0133
107 | (MyValueError("This is an exception")) # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:112:12
|
110 | # Test case 10: Useless exception statement in continuation
111 | def func():
112 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
|
help: Add `raise` keyword
109 |
110 | # Test case 10: Useless exception statement in continuation
111 | def func():
- x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
112 + x = 1; raise (RuntimeError("This is an exception")); y = 2 # PLW0133
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:120:5
|
118 | # Test case 11: Useless warning statement
119 | def func():
120 | UserWarning("This is a user warning") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
121 | MyUserWarning("This is a custom user warning") # PLW0133
|
help: Add `raise` keyword
117 |
118 | # Test case 11: Useless warning statement
119 | def func():
- UserWarning("This is a user warning") # PLW0133
120 + raise UserWarning("This is a user warning") # PLW0133
121 | MyUserWarning("This is a custom user warning") # PLW0133
122 |
123 |
124 | import builtins
125 |
- builtins.TypeError("still an exception even though it's an Attribute")
126 + raise builtins.TypeError("still an exception even though it's an Attribute")
127 |
128 | PythonFinalizationError("Added in Python 3.13")
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:128:1
--> useless_exception_statement.py:127:1
|
126 | builtins.TypeError("still an exception even though it's an Attribute")
127 |
128 | PythonFinalizationError("Added in Python 3.13")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
125 | import builtins
126 |
127 | builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
128 |
129 | PythonFinalizationError("Added in Python 3.13") # PLW0133
|
help: Add `raise` keyword
125 |
126 | builtins.TypeError("still an exception even though it's an Attribute")
127 |
- PythonFinalizationError("Added in Python 3.13")
128 + raise PythonFinalizationError("Added in Python 3.13")
124 | # Test case 12: Useless exception statement at module level
125 | import builtins
126 |
- builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
127 + raise builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
128 |
129 | PythonFinalizationError("Added in Python 3.13") # PLW0133
130 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:129:1
|
127 | builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
128 |
129 | PythonFinalizationError("Added in Python 3.13") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
130 |
131 | MyError("This is an exception") # PLW0133
|
help: Add `raise` keyword
126 |
127 | builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
128 |
- PythonFinalizationError("Added in Python 3.13") # PLW0133
129 + raise PythonFinalizationError("Added in Python 3.13") # PLW0133
130 |
131 | MyError("This is an exception") # PLW0133
132 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:137:1
|
135 | MyValueError("This is an exception") # PLW0133
136 |
137 | UserWarning("This is a user warning") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
138 |
139 | MyUserWarning("This is a custom user warning") # PLW0133
|
help: Add `raise` keyword
134 |
135 | MyValueError("This is an exception") # PLW0133
136 |
- UserWarning("This is a user warning") # PLW0133
137 + raise UserWarning("This is a user warning") # PLW0133
138 |
139 | MyUserWarning("This is a custom user warning") # PLW0133
140 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -0,0 +1,751 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 35
--- Added ---
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:27:5
|
25 | def func():
26 | AssertionError("This is an assertion error") # PLW0133
27 | MyError("This is a custom error") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | MySubError("This is a custom error") # PLW0133
29 | MyValueError("This is a custom value error") # PLW0133
|
help: Add `raise` keyword
24 | # Test case 1: Useless exception statement
25 | def func():
26 | AssertionError("This is an assertion error") # PLW0133
- MyError("This is a custom error") # PLW0133
27 + raise MyError("This is a custom error") # PLW0133
28 | MySubError("This is a custom error") # PLW0133
29 | MyValueError("This is a custom value error") # PLW0133
30 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:28:5
|
26 | AssertionError("This is an assertion error") # PLW0133
27 | MyError("This is a custom error") # PLW0133
28 | MySubError("This is a custom error") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
29 | MyValueError("This is a custom value error") # PLW0133
|
help: Add `raise` keyword
25 | def func():
26 | AssertionError("This is an assertion error") # PLW0133
27 | MyError("This is a custom error") # PLW0133
- MySubError("This is a custom error") # PLW0133
28 + raise MySubError("This is a custom error") # PLW0133
29 | MyValueError("This is a custom value error") # PLW0133
30 |
31 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:29:5
|
27 | MyError("This is a custom error") # PLW0133
28 | MySubError("This is a custom error") # PLW0133
29 | MyValueError("This is a custom value error") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
26 | AssertionError("This is an assertion error") # PLW0133
27 | MyError("This is a custom error") # PLW0133
28 | MySubError("This is a custom error") # PLW0133
- MyValueError("This is a custom value error") # PLW0133
29 + raise MyValueError("This is a custom value error") # PLW0133
30 |
31 |
32 | # Test case 2: Useless exception statement in try-except block
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:36:9
|
34 | try:
35 | Exception("This is an exception") # PLW0133
36 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
37 | MySubError("This is an exception") # PLW0133
38 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
33 | def func():
34 | try:
35 | Exception("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
36 + raise MyError("This is an exception") # PLW0133
37 | MySubError("This is an exception") # PLW0133
38 | MyValueError("This is an exception") # PLW0133
39 | except Exception as err:
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:37:9
|
35 | Exception("This is an exception") # PLW0133
36 | MyError("This is an exception") # PLW0133
37 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
38 | MyValueError("This is an exception") # PLW0133
39 | except Exception as err:
|
help: Add `raise` keyword
34 | try:
35 | Exception("This is an exception") # PLW0133
36 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
37 + raise MySubError("This is an exception") # PLW0133
38 | MyValueError("This is an exception") # PLW0133
39 | except Exception as err:
40 | pass
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:38:9
|
36 | MyError("This is an exception") # PLW0133
37 | MySubError("This is an exception") # PLW0133
38 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39 | except Exception as err:
40 | pass
|
help: Add `raise` keyword
35 | Exception("This is an exception") # PLW0133
36 | MyError("This is an exception") # PLW0133
37 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
38 + raise MyValueError("This is an exception") # PLW0133
39 | except Exception as err:
40 | pass
41 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:47:9
|
45 | if True:
46 | RuntimeError("This is an exception") # PLW0133
47 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
48 | MySubError("This is an exception") # PLW0133
49 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
44 | def func():
45 | if True:
46 | RuntimeError("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
47 + raise MyError("This is an exception") # PLW0133
48 | MySubError("This is an exception") # PLW0133
49 | MyValueError("This is an exception") # PLW0133
50 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:48:9
|
46 | RuntimeError("This is an exception") # PLW0133
47 | MyError("This is an exception") # PLW0133
48 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
45 | if True:
46 | RuntimeError("This is an exception") # PLW0133
47 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
48 + raise MySubError("This is an exception") # PLW0133
49 | MyValueError("This is an exception") # PLW0133
50 |
51 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:49:9
|
47 | MyError("This is an exception") # PLW0133
48 | MySubError("This is an exception") # PLW0133
49 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
46 | RuntimeError("This is an exception") # PLW0133
47 | MyError("This is an exception") # PLW0133
48 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
49 + raise MyValueError("This is an exception") # PLW0133
50 |
51 |
52 | # Test case 4: Useless exception statement in class
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:57:13
|
55 | def __init__(self):
56 | TypeError("This is an exception") # PLW0133
57 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 | MySubError("This is an exception") # PLW0133
59 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
54 | class Class:
55 | def __init__(self):
56 | TypeError("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
57 + raise MyError("This is an exception") # PLW0133
58 | MySubError("This is an exception") # PLW0133
59 | MyValueError("This is an exception") # PLW0133
60 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:58:13
|
56 | TypeError("This is an exception") # PLW0133
57 | MyError("This is an exception") # PLW0133
58 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
55 | def __init__(self):
56 | TypeError("This is an exception") # PLW0133
57 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
58 + raise MySubError("This is an exception") # PLW0133
59 | MyValueError("This is an exception") # PLW0133
60 |
61 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:59:13
|
57 | MyError("This is an exception") # PLW0133
58 | MySubError("This is an exception") # PLW0133
59 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
56 | TypeError("This is an exception") # PLW0133
57 | MyError("This is an exception") # PLW0133
58 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
59 + raise MyValueError("This is an exception") # PLW0133
60 |
61 |
62 | # Test case 5: Useless exception statement in function
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:66:9
|
64 | def inner():
65 | IndexError("This is an exception") # PLW0133
66 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
67 | MySubError("This is an exception") # PLW0133
68 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
63 | def func():
64 | def inner():
65 | IndexError("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
66 + raise MyError("This is an exception") # PLW0133
67 | MySubError("This is an exception") # PLW0133
68 | MyValueError("This is an exception") # PLW0133
69 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:67:9
|
65 | IndexError("This is an exception") # PLW0133
66 | MyError("This is an exception") # PLW0133
67 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
68 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
64 | def inner():
65 | IndexError("This is an exception") # PLW0133
66 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
67 + raise MySubError("This is an exception") # PLW0133
68 | MyValueError("This is an exception") # PLW0133
69 |
70 | inner()
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:68:9
|
66 | MyError("This is an exception") # PLW0133
67 | MySubError("This is an exception") # PLW0133
68 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
69 |
70 | inner()
|
help: Add `raise` keyword
65 | IndexError("This is an exception") # PLW0133
66 | MyError("This is an exception") # PLW0133
67 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
68 + raise MyValueError("This is an exception") # PLW0133
69 |
70 | inner()
71 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:77:9
|
75 | while True:
76 | KeyError("This is an exception") # PLW0133
77 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
78 | MySubError("This is an exception") # PLW0133
79 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
74 | def func():
75 | while True:
76 | KeyError("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
77 + raise MyError("This is an exception") # PLW0133
78 | MySubError("This is an exception") # PLW0133
79 | MyValueError("This is an exception") # PLW0133
80 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:78:9
|
76 | KeyError("This is an exception") # PLW0133
77 | MyError("This is an exception") # PLW0133
78 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
79 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
75 | while True:
76 | KeyError("This is an exception") # PLW0133
77 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
78 + raise MySubError("This is an exception") # PLW0133
79 | MyValueError("This is an exception") # PLW0133
80 |
81 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:79:9
|
77 | MyError("This is an exception") # PLW0133
78 | MySubError("This is an exception") # PLW0133
79 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
76 | KeyError("This is an exception") # PLW0133
77 | MyError("This is an exception") # PLW0133
78 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
79 + raise MyValueError("This is an exception") # PLW0133
80 |
81 |
82 | # Test case 7: Useless exception statement in abstract class
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:88:13
|
86 | def method(self):
87 | NotImplementedError("This is an exception") # PLW0133
88 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
89 | MySubError("This is an exception") # PLW0133
90 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
85 | @abstractmethod
86 | def method(self):
87 | NotImplementedError("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
88 + raise MyError("This is an exception") # PLW0133
89 | MySubError("This is an exception") # PLW0133
90 | MyValueError("This is an exception") # PLW0133
91 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:89:13
|
87 | NotImplementedError("This is an exception") # PLW0133
88 | MyError("This is an exception") # PLW0133
89 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
90 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
86 | def method(self):
87 | NotImplementedError("This is an exception") # PLW0133
88 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
89 + raise MySubError("This is an exception") # PLW0133
90 | MyValueError("This is an exception") # PLW0133
91 |
92 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:90:13
|
88 | MyError("This is an exception") # PLW0133
89 | MySubError("This is an exception") # PLW0133
90 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
87 | NotImplementedError("This is an exception") # PLW0133
88 | MyError("This is an exception") # PLW0133
89 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
90 + raise MyValueError("This is an exception") # PLW0133
91 |
92 |
93 | # Test case 8: Useless exception statement inside context manager
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:97:9
|
95 | with suppress(Exception):
96 | AttributeError("This is an exception") # PLW0133
97 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
98 | MySubError("This is an exception") # PLW0133
99 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
94 | def func():
95 | with suppress(Exception):
96 | AttributeError("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
97 + raise MyError("This is an exception") # PLW0133
98 | MySubError("This is an exception") # PLW0133
99 | MyValueError("This is an exception") # PLW0133
100 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:98:9
|
96 | AttributeError("This is an exception") # PLW0133
97 | MyError("This is an exception") # PLW0133
98 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
99 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
95 | with suppress(Exception):
96 | AttributeError("This is an exception") # PLW0133
97 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
98 + raise MySubError("This is an exception") # PLW0133
99 | MyValueError("This is an exception") # PLW0133
100 |
101 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:99:9
|
97 | MyError("This is an exception") # PLW0133
98 | MySubError("This is an exception") # PLW0133
99 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
96 | AttributeError("This is an exception") # PLW0133
97 | MyError("This is an exception") # PLW0133
98 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
99 + raise MyValueError("This is an exception") # PLW0133
100 |
101 |
102 | # Test case 9: Useless exception statement in parentheses
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:105:5
|
103 | def func():
104 | (RuntimeError("This is an exception")) # PLW0133
105 | (MyError("This is an exception")) # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
106 | (MySubError("This is an exception")) # PLW0133
107 | (MyValueError("This is an exception")) # PLW0133
|
help: Add `raise` keyword
102 | # Test case 9: Useless exception statement in parentheses
103 | def func():
104 | (RuntimeError("This is an exception")) # PLW0133
- (MyError("This is an exception")) # PLW0133
105 + raise (MyError("This is an exception")) # PLW0133
106 | (MySubError("This is an exception")) # PLW0133
107 | (MyValueError("This is an exception")) # PLW0133
108 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:106:5
|
104 | (RuntimeError("This is an exception")) # PLW0133
105 | (MyError("This is an exception")) # PLW0133
106 | (MySubError("This is an exception")) # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
107 | (MyValueError("This is an exception")) # PLW0133
|
help: Add `raise` keyword
103 | def func():
104 | (RuntimeError("This is an exception")) # PLW0133
105 | (MyError("This is an exception")) # PLW0133
- (MySubError("This is an exception")) # PLW0133
106 + raise (MySubError("This is an exception")) # PLW0133
107 | (MyValueError("This is an exception")) # PLW0133
108 |
109 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:107:5
|
105 | (MyError("This is an exception")) # PLW0133
106 | (MySubError("This is an exception")) # PLW0133
107 | (MyValueError("This is an exception")) # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
104 | (RuntimeError("This is an exception")) # PLW0133
105 | (MyError("This is an exception")) # PLW0133
106 | (MySubError("This is an exception")) # PLW0133
- (MyValueError("This is an exception")) # PLW0133
107 + raise (MyValueError("This is an exception")) # PLW0133
108 |
109 |
110 | # Test case 10: Useless exception statement in continuation
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:113:12
|
111 | def func():
112 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
|
help: Add `raise` keyword
110 | # Test case 10: Useless exception statement in continuation
111 | def func():
112 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
- x = 1; (MyError("This is an exception")); y = 2 # PLW0133
113 + x = 1; raise (MyError("This is an exception")); y = 2 # PLW0133
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
116 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:114:12
|
112 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
|
help: Add `raise` keyword
111 | def func():
112 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
- x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
114 + x = 1; raise (MySubError("This is an exception")); y = 2 # PLW0133
115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
116 |
117 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:115:12
|
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
112 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
- x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
115 + x = 1; raise (MyValueError("This is an exception")); y = 2 # PLW0133
116 |
117 |
118 | # Test case 11: Useless warning statement
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:121:5
|
119 | def func():
120 | UserWarning("This is a user warning") # PLW0133
121 | MyUserWarning("This is a custom user warning") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
118 | # Test case 11: Useless warning statement
119 | def func():
120 | UserWarning("This is a user warning") # PLW0133
- MyUserWarning("This is a custom user warning") # PLW0133
121 + raise MyUserWarning("This is a custom user warning") # PLW0133
122 |
123 |
124 | # Test case 12: Useless exception statement at module level
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:131:1
|
129 | PythonFinalizationError("Added in Python 3.13") # PLW0133
130 |
131 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
132 |
133 | MySubError("This is an exception") # PLW0133
|
help: Add `raise` keyword
128 |
129 | PythonFinalizationError("Added in Python 3.13") # PLW0133
130 |
- MyError("This is an exception") # PLW0133
131 + raise MyError("This is an exception") # PLW0133
132 |
133 | MySubError("This is an exception") # PLW0133
134 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:133:1
|
131 | MyError("This is an exception") # PLW0133
132 |
133 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
134 |
135 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
130 |
131 | MyError("This is an exception") # PLW0133
132 |
- MySubError("This is an exception") # PLW0133
133 + raise MySubError("This is an exception") # PLW0133
134 |
135 | MyValueError("This is an exception") # PLW0133
136 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:135:1
|
133 | MySubError("This is an exception") # PLW0133
134 |
135 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
136 |
137 | UserWarning("This is a user warning") # PLW0133
|
help: Add `raise` keyword
132 |
133 | MySubError("This is an exception") # PLW0133
134 |
- MyValueError("This is an exception") # PLW0133
135 + raise MyValueError("This is an exception") # PLW0133
136 |
137 | UserWarning("This is a user warning") # PLW0133
138 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:139:1
|
137 | UserWarning("This is a user warning") # PLW0133
138 |
139 | MyUserWarning("This is a custom user warning") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
136 |
137 | UserWarning("This is a user warning") # PLW0133
138 |
- MyUserWarning("This is a custom user warning") # PLW0133
139 + raise MyUserWarning("This is a custom user warning") # PLW0133
140 |
141 |
142 | # Non-violation test cases: PLW0133
note: This is an unsafe fix and may change runtime behavior

View File

@@ -204,7 +204,7 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD
arguments,
Parentheses::Remove,
checker.source(),
checker.comment_ranges(),
checker.tokens(),
)?;
Ok(Fix::unsafe_edits(
Edit::insertion(type_params.to_string(), name.end()),

View File

@@ -2,7 +2,7 @@ use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::name::Name;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{Expr, ExprCall, ExprName, Keyword, StmtAnnAssign, StmtAssign, StmtRef};
use ruff_text_size::{Ranged, TextRange};
@@ -261,11 +261,11 @@ fn create_diagnostic(
type_alias_kind: TypeAliasKind,
) {
let source = checker.source();
let tokens = checker.tokens();
let comment_ranges = checker.comment_ranges();
let range_with_parentheses =
parenthesized_range(value.into(), stmt.into(), comment_ranges, source)
.unwrap_or(value.range());
parenthesized_range(value.into(), stmt.into(), tokens).unwrap_or(value.range());
let content = format!(
"type {name}{type_params} = {value}",

View File

@@ -1,9 +1,8 @@
use anyhow::Result;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Keyword};
use ruff_python_ast::{self as ast, Keyword, token::Tokens};
use ruff_python_semantic::Modules;
use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -104,7 +103,7 @@ pub(crate) fn replace_stdout_stderr(checker: &Checker, call: &ast::ExprCall) {
stderr,
call,
checker.locator().contents(),
checker.comment_ranges(),
checker.tokens(),
)
});
}
@@ -117,7 +116,7 @@ fn generate_fix(
stderr: &Keyword,
call: &ast::ExprCall,
source: &str,
comment_ranges: &CommentRanges,
tokens: &Tokens,
) -> Result<Fix> {
let (first, second) = if stdout.start() < stderr.start() {
(stdout, stderr)
@@ -132,7 +131,7 @@ fn generate_fix(
&call.arguments,
Parentheses::Preserve,
source,
comment_ranges,
tokens,
)?],
))
}

View File

@@ -78,7 +78,7 @@ pub(crate) fn replace_universal_newlines(checker: &Checker, call: &ast::ExprCall
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
checker.comment_ranges(),
checker.tokens(),
)
.map(Fix::safe_edit)
});

View File

@@ -188,7 +188,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) {
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
checker.comment_ranges(),
checker.tokens(),
)
.map(Fix::safe_edit)
});
@@ -206,7 +206,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) {
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
checker.comment_ranges(),
checker.tokens(),
)
.map(Fix::safe_edit)
});
@@ -231,7 +231,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) {
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
checker.comment_ranges(),
checker.tokens(),
)
.map(Fix::safe_edit)
});
@@ -249,7 +249,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) {
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
checker.comment_ranges(),
checker.tokens(),
)
.map(Fix::safe_edit)
});

View File

@@ -70,7 +70,7 @@ pub(crate) fn useless_class_metaclass_type(checker: &Checker, class_def: &StmtCl
arguments,
Parentheses::Remove,
checker.locator().contents(),
checker.comment_ranges(),
checker.tokens(),
)?;
let range = edit.range();

View File

@@ -73,7 +73,7 @@ pub(crate) fn useless_object_inheritance(checker: &Checker, class_def: &ast::Stm
arguments,
Parentheses::Remove,
checker.locator().contents(),
checker.comment_ranges(),
checker.tokens(),
)?;
let range = edit.range();

View File

@@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_text_size::Ranged;
@@ -139,13 +139,8 @@ pub(crate) fn yield_in_for_loop(checker: &Checker, stmt_for: &ast::StmtFor) {
let mut diagnostic = checker.report_diagnostic(YieldInForLoop, stmt_for.range());
let contents = checker.locator().slice(
parenthesized_range(
iter.as_ref().into(),
stmt_for.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(iter.range()),
parenthesized_range(iter.as_ref().into(), stmt_for.into(), checker.tokens())
.unwrap_or(iter.range()),
);
let contents = if iter.as_tuple_expr().is_some_and(|it| !it.parenthesized) {
format!("yield from ({contents})")

View File

@@ -1,7 +1,7 @@
use std::borrow::Cow;
use ruff_python_ast::PythonVersion;
use ruff_python_ast::{self as ast, Expr, name::Name, parenthesize::parenthesized_range};
use ruff_python_ast::{self as ast, Expr, name::Name, token::parenthesized_range};
use ruff_python_codegen::Generator;
use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel};
use ruff_text_size::{Ranged, TextRange};
@@ -330,12 +330,8 @@ pub(super) fn parenthesize_loop_iter_if_necessary<'a>(
let locator = checker.locator();
let iter = for_stmt.iter.as_ref();
let original_parenthesized_range = parenthesized_range(
iter.into(),
for_stmt.into(),
checker.comment_ranges(),
checker.source(),
);
let original_parenthesized_range =
parenthesized_range(iter.into(), for_stmt.into(), checker.tokens());
if let Some(range) = original_parenthesized_range {
return Cow::Borrowed(locator.slice(range));

View File

@@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{
Expr, ExprAttribute, ExprBinOp, ExprCall, ExprStringLiteral, ExprSubscript, ExprUnaryOp,
Number, Operator, PythonVersion, UnaryOp,
@@ -112,8 +112,7 @@ pub(crate) fn fromisoformat_replace_z(checker: &Checker, call: &ExprCall) {
let value_full_range = parenthesized_range(
replace_time_zone.date.into(),
replace_time_zone.parent.into(),
checker.comment_ranges(),
checker.source(),
checker.tokens(),
)
.unwrap_or(replace_time_zone.date.range());

View File

@@ -5,8 +5,7 @@ use ruff_python_ast as ast;
use ruff_python_ast::Expr;
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_trivia::CommentRanges;
use ruff_python_ast::token::{Tokens, parenthesized_range};
use ruff_text_size::Ranged;
use crate::Locator;
@@ -76,8 +75,8 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &Checker, if_expr: &ast::Ex
Edit::range_replacement(
format!(
"{} or {}",
parenthesize_test(test, if_expr, checker.comment_ranges(), checker.locator()),
parenthesize_test(orelse, if_expr, checker.comment_ranges(), checker.locator()),
parenthesize_test(test, if_expr, checker.tokens(), checker.locator()),
parenthesize_test(orelse, if_expr, checker.tokens(), checker.locator()),
),
if_expr.range(),
),
@@ -99,15 +98,10 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &Checker, if_expr: &ast::Ex
fn parenthesize_test<'a>(
expr: &Expr,
if_expr: &ast::ExprIf,
comment_ranges: &CommentRanges,
tokens: &Tokens,
locator: &Locator<'a>,
) -> Cow<'a, str> {
if let Some(range) = parenthesized_range(
expr.into(),
if_expr.into(),
comment_ranges,
locator.contents(),
) {
if let Some(range) = parenthesized_range(expr.into(), if_expr.into(), tokens) {
Cow::Borrowed(locator.slice(range))
} else if matches!(expr, Expr::If(_) | Expr::Lambda(_) | Expr::Named(_)) {
Cow::Owned(format!("({})", locator.slice(expr.range())))

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{Comprehension, Expr, StmtFor};
use ruff_python_semantic::analyze::typing;
use ruff_python_semantic::analyze::typing::is_io_base_expr;
@@ -104,8 +104,7 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) {
let deletion_range = if let Some(parenthesized_range) = parenthesized_range(
expr_attr.value.as_ref().into(),
expr_attr.into(),
checker.comment_ranges(),
checker.source(),
checker.tokens(),
) {
expr_call.range().add_start(parenthesized_range.len())
} else {

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Number};
use ruff_text_size::Ranged;
@@ -152,13 +152,8 @@ fn generate_fix(checker: &Checker, call: &ast::ExprCall, base: Base, arg: &Expr)
checker.semantic(),
)?;
let arg_range = parenthesized_range(
arg.into(),
call.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(arg.range());
let arg_range =
parenthesized_range(arg.into(), call.into(), checker.tokens()).unwrap_or(arg.range());
let arg_str = checker.locator().slice(arg_range);
Ok(Fix::applicable_edits(

View File

@@ -95,7 +95,7 @@ pub(crate) fn single_item_membership_test(
&[membership_test.replacement_op()],
std::slice::from_ref(item),
expr.into(),
checker.comment_ranges(),
checker.tokens(),
checker.source(),
),
expr.range(),

View File

@@ -163,7 +163,7 @@ fn convert_type_vars(
class_arguments,
Parentheses::Remove,
source,
checker.comment_ranges(),
checker.tokens(),
)?;
let replace_type_params =
Edit::range_replacement(new_type_params.to_string(), type_params.range);

View File

@@ -3,8 +3,8 @@ use anyhow::Result;
use ast::Keyword;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_constant;
use ruff_python_ast::token::Tokens;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged;
use crate::Locator;
@@ -108,9 +108,8 @@ pub(crate) fn default_factory_kwarg(checker: &Checker, call: &ast::ExprCall) {
},
call.range(),
);
diagnostic.try_set_fix(|| {
convert_to_positional(call, keyword, checker.locator(), checker.comment_ranges())
});
diagnostic
.try_set_fix(|| convert_to_positional(call, keyword, checker.locator(), checker.tokens()));
}
/// Returns `true` if a value is definitively not callable (e.g., `1` or `[]`).
@@ -136,7 +135,7 @@ fn convert_to_positional(
call: &ast::ExprCall,
default_factory: &Keyword,
locator: &Locator,
comment_ranges: &CommentRanges,
tokens: &Tokens,
) -> Result<Fix> {
if call.arguments.len() == 1 {
// Ex) `defaultdict(default_factory=list)`
@@ -153,7 +152,7 @@ fn convert_to_positional(
&call.arguments,
Parentheses::Preserve,
locator.contents(),
comment_ranges,
tokens,
)?;
// Second, insert the value as the first positional argument.

View File

@@ -128,7 +128,7 @@ pub(crate) fn falsy_dict_get_fallback(checker: &Checker, expr: &Expr) {
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
checker.comment_ranges(),
checker.tokens(),
)
.map(|edit| Fix::applicable_edit(edit, applicability))
});

View File

@@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -77,14 +77,7 @@ pub(crate) fn parenthesize_chained_logical_operators(checker: &Checker, expr: &a
) => {
let locator = checker.locator();
let source_range = bool_op.range();
if parenthesized_range(
bool_op.into(),
expr.into(),
checker.comment_ranges(),
locator.contents(),
)
.is_none()
{
if parenthesized_range(bool_op.into(), expr.into(), checker.tokens()).is_none() {
let new_source = format!("({})", locator.slice(source_range));
let edit = Edit::range_replacement(new_source, source_range);
checker

View File

@@ -2,7 +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_ast::token::parenthesized_range;
use ruff_python_semantic::{Scope, ScopeKind};
use ruff_python_trivia::{indentation_at_offset, textwrap};
use ruff_source_file::LineRanges;
@@ -159,8 +159,7 @@ fn use_initvar(
let default_loc = parenthesized_range(
default.into(),
parameter_with_default.into(),
checker.comment_ranges(),
checker.source(),
checker.tokens(),
)
.unwrap_or(default.range());

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
@@ -116,13 +116,8 @@ fn convert_to_reduce(iterable: &Expr, call: &ast::ExprCall, checker: &Checker) -
)?;
let iterable = checker.locator().slice(
parenthesized_range(
iterable.into(),
(&call.arguments).into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(iterable.range()),
parenthesized_range(iterable.into(), (&call.arguments).into(), checker.tokens())
.unwrap_or(iterable.range()),
);
Ok(Fix::unsafe_edits(

View File

@@ -1,7 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::PythonVersion;
use ruff_python_ast::token::TokenKind;
use ruff_python_ast::{Expr, ExprCall, parenthesize::parenthesized_range};
use ruff_python_ast::{Expr, ExprCall, token::parenthesized_range};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@@ -124,13 +124,8 @@ fn replace_with_map(starmap: &ExprCall, zip: &ExprCall, checker: &Checker) -> Op
let mut remove_zip = vec![];
let full_zip_range = parenthesized_range(
zip.into(),
starmap.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(zip.range());
let full_zip_range =
parenthesized_range(zip.into(), starmap.into(), checker.tokens()).unwrap_or(zip.range());
// Delete any parentheses around the `zip` call to prevent that the argument turns into a tuple.
remove_zip.push(Edit::range_deletion(TextRange::new(
@@ -138,13 +133,8 @@ fn replace_with_map(starmap: &ExprCall, zip: &ExprCall, checker: &Checker) -> Op
zip.start(),
)));
let full_zip_func_range = parenthesized_range(
(&zip.func).into(),
zip.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(zip.func.range());
let full_zip_func_range = parenthesized_range((&zip.func).into(), zip.into(), checker.tokens())
.unwrap_or(zip.func.range());
// Delete the `zip` callee
remove_zip.push(Edit::range_deletion(full_zip_func_range));

View File

@@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::{Tokens, parenthesized_range};
use ruff_python_ast::{Arguments, Expr, ExprCall};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
@@ -86,6 +86,7 @@ pub(crate) fn unnecessary_cast_to_int(checker: &Checker, call: &ExprCall) {
applicability,
checker.semantic(),
checker.locator(),
checker.tokens(),
checker.comment_ranges(),
checker.source(),
);
@@ -95,27 +96,26 @@ pub(crate) fn unnecessary_cast_to_int(checker: &Checker, call: &ExprCall) {
}
/// Creates a fix that replaces `int(expression)` with `expression`.
#[allow(clippy::too_many_arguments)]
fn unwrap_int_expression(
call: &ExprCall,
argument: &Expr,
applicability: Applicability,
semantic: &SemanticModel,
locator: &Locator,
tokens: &Tokens,
comment_ranges: &CommentRanges,
source: &str,
) -> Fix {
let content = if let Some(range) = parenthesized_range(
argument.into(),
(&call.arguments).into(),
comment_ranges,
source,
) {
let content = if let Some(range) =
parenthesized_range(argument.into(), (&call.arguments).into(), tokens)
{
locator.slice(range).to_string()
} else {
let parenthesize = semantic.current_expression_parent().is_some()
|| argument.is_named_expr()
|| locator.count_lines(argument.range()) > 0;
if parenthesize && !has_own_parentheses(argument, comment_ranges, source) {
if parenthesize && !has_own_parentheses(argument, tokens, source) {
format!("({})", locator.slice(argument.range()))
} else {
locator.slice(argument.range()).to_string()
@@ -255,7 +255,7 @@ fn round_applicability(arguments: &Arguments, semantic: &SemanticModel) -> Optio
}
/// Returns `true` if the given [`Expr`] has its own parentheses (e.g., `()`, `[]`, `{}`).
fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str) -> bool {
fn has_own_parentheses(expr: &Expr, tokens: &Tokens, source: &str) -> bool {
match expr {
Expr::ListComp(_)
| Expr::SetComp(_)
@@ -276,14 +276,10 @@ fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str
// f
// (10)
// ```
let func_end = parenthesized_range(
call_expr.func.as_ref().into(),
call_expr.into(),
comment_ranges,
source,
)
.unwrap_or(call_expr.func.range())
.end();
let func_end =
parenthesized_range(call_expr.func.as_ref().into(), call_expr.into(), tokens)
.unwrap_or(call_expr.func.range())
.end();
lines_after_ignoring_trivia(func_end, source) == 0
}
Expr::Subscript(subscript_expr) => {
@@ -291,8 +287,7 @@ fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str
let subscript_end = parenthesized_range(
subscript_expr.value.as_ref().into(),
subscript_expr.into(),
comment_ranges,
source,
tokens,
)
.unwrap_or(subscript_expr.value.range())
.end();

View File

@@ -3,7 +3,7 @@ use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -108,22 +108,12 @@ pub(crate) fn unnecessary_key_check(checker: &Checker, expr: &Expr) {
format!(
"{}.get({})",
checker.locator().slice(
parenthesized_range(
obj_right.into(),
right.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(obj_right.range())
parenthesized_range(obj_right.into(), right.into(), checker.tokens(),)
.unwrap_or(obj_right.range())
),
checker.locator().slice(
parenthesized_range(
key_right.into(),
right.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(key_right.range())
parenthesized_range(key_right.into(), right.into(), checker.tokens(),)
.unwrap_or(key_right.range())
),
),
expr.range(),

View File

@@ -2,7 +2,7 @@ use ruff_diagnostics::{Applicability, Edit};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_empty_f_string;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
@@ -140,31 +140,19 @@ fn fix_unnecessary_literal_in_deque(
// call. otherwise, we only delete the `iterable` argument and leave the others untouched.
let edit = if let Some(maxlen) = maxlen {
let deque_name = checker.locator().slice(
parenthesized_range(
deque.func.as_ref().into(),
deque.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(deque.func.range()),
parenthesized_range(deque.func.as_ref().into(), deque.into(), checker.tokens())
.unwrap_or(deque.func.range()),
);
let len_str = checker.locator().slice(maxlen);
let deque_str = format!("{deque_name}(maxlen={len_str})");
Edit::range_replacement(deque_str, deque.range)
} else {
let range = parenthesized_range(
iterable.value().into(),
(&deque.arguments).into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(iterable.range());
remove_argument(
&range,
&iterable,
&deque.arguments,
Parentheses::Preserve,
checker.source(),
checker.comment_ranges(),
checker.tokens(),
)?
};
let has_comments = checker.comment_ranges().intersects(edit.range());

View File

@@ -4,7 +4,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::AlwaysFixableViolation;
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Default)]
pub(crate) struct UnusedCodes {
pub disabled: Vec<String>,
pub duplicated: Vec<String>,
@@ -12,6 +12,21 @@ pub(crate) struct UnusedCodes {
pub unmatched: Vec<String>,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum UnusedNOQAKind {
Noqa,
Suppression,
}
impl UnusedNOQAKind {
fn as_str(&self) -> &str {
match self {
UnusedNOQAKind::Noqa => "`noqa` directive",
UnusedNOQAKind::Suppression => "suppression",
}
}
}
/// ## What it does
/// Checks for `noqa` directives that are no longer applicable.
///
@@ -46,6 +61,7 @@ pub(crate) struct UnusedCodes {
#[violation_metadata(stable_since = "v0.0.155")]
pub(crate) struct UnusedNOQA {
pub codes: Option<UnusedCodes>,
pub kind: UnusedNOQAKind,
}
impl AlwaysFixableViolation for UnusedNOQA {
@@ -95,16 +111,20 @@ impl AlwaysFixableViolation for UnusedNOQA {
));
}
if codes_by_reason.is_empty() {
"Unused `noqa` directive".to_string()
format!("Unused {}", self.kind.as_str())
} else {
format!("Unused `noqa` directive ({})", codes_by_reason.join("; "))
format!(
"Unused {} ({})",
self.kind.as_str(),
codes_by_reason.join("; ")
)
}
}
None => "Unused blanket `noqa` directive".to_string(),
None => format!("Unused blanket {}", self.kind.as_str()),
}
}
fn fix_title(&self) -> String {
"Remove unused `noqa` directive".to_string()
format!("Remove unused {}", self.kind.as_str())
}
}

View File

@@ -6,8 +6,8 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs
+linter.preview = enabled
--- Summary ---
Removed: 9
Added: 1
Removed: 14
Added: 11
--- Removed ---
E741 Ambiguous variable name: `I`
@@ -148,8 +148,136 @@ help: Remove assignment to unused variable `I`
note: This is an unsafe fix and may change runtime behavior
F841 [*] Local variable `foo` is assigned to but never used
--> suppressions.py:62:5
|
60 | # TODO: Duplicate codes should be counted as duplicate, not unused
61 | # ruff: disable[F841, F841]
62 | foo = 0
| ^^^
|
help: Remove assignment to unused variable `foo`
59 | def f():
60 | # TODO: Duplicate codes should be counted as duplicate, not unused
61 | # ruff: disable[F841, F841]
- foo = 0
62 + pass
63 |
64 |
65 | def f():
note: This is an unsafe fix and may change runtime behavior
F841 [*] Local variable `foo` is assigned to but never used
--> suppressions.py:70:5
|
68 | # ruff: disable[F841]
69 | # ruff: disable[F841]
70 | foo = 0
| ^^^
|
help: Remove assignment to unused variable `foo`
67 | # and the other should trigger an unused suppression diagnostic
68 | # ruff: disable[F841]
69 | # ruff: disable[F841]
- foo = 0
70 + pass
71 |
72 |
73 | def f():
note: This is an unsafe fix and may change runtime behavior
F841 [*] Local variable `foo` is assigned to but never used
--> suppressions.py:76:5
|
74 | # Multiple codes but only one is used
75 | # ruff: disable[E741, F401, F841]
76 | foo = 0
| ^^^
|
help: Remove assignment to unused variable `foo`
73 | def f():
74 | # Multiple codes but only one is used
75 | # ruff: disable[E741, F401, F841]
- foo = 0
76 + pass
77 |
78 |
79 | def f():
note: This is an unsafe fix and may change runtime behavior
E741 Ambiguous variable name: `I`
--> suppressions.py:82:5
|
80 | # Multiple codes but only two are used
81 | # ruff: disable[E741, F401, F841]
82 | I = 0
| ^
|
F841 [*] Local variable `I` is assigned to but never used
--> suppressions.py:82:5
|
80 | # Multiple codes but only two are used
81 | # ruff: disable[E741, F401, F841]
82 | I = 0
| ^
|
help: Remove assignment to unused variable `I`
79 | def f():
80 | # Multiple codes but only two are used
81 | # ruff: disable[E741, F401, F841]
- I = 0
82 + pass
83 |
84 |
85 | def f():
note: This is an unsafe fix and may change runtime behavior
--- Added ---
RUF100 [*] Unused suppression (non-enabled: `E501`)
--> suppressions.py:46:5
|
44 | # Neither of these are ignored and warnings are
45 | # logged to user
46 | # ruff: disable[E501]
| ^^^^^^^^^^^^^^^^^^^^^
47 | I = 1
48 | # ruff: enable[E501]
|
help: Remove unused suppression
43 | def f():
44 | # Neither of these are ignored and warnings are
45 | # logged to user
- # ruff: disable[E501]
46 | I = 1
47 | # ruff: enable[E501]
48 |
RUF100 [*] Unused suppression (non-enabled: `E501`)
--> suppressions.py:48:5
|
46 | # ruff: disable[E501]
47 | I = 1
48 | # ruff: enable[E501]
| ^^^^^^^^^^^^^^^^^^^^
|
help: Remove unused suppression
45 | # logged to user
46 | # ruff: disable[E501]
47 | I = 1
- # ruff: enable[E501]
48 |
49 |
50 | def f():
RUF100 [*] Unused `noqa` directive (unused: `E741`, `F841`)
--> suppressions.py:55:12
|
@@ -166,3 +294,158 @@ help: Remove unused `noqa` directive
- I = 1 # noqa: E741,F841
55 + I = 1
56 | # ruff:enable[E741,F841]
57 |
58 |
RUF100 [*] Unused suppression (unused: `F841`)
--> suppressions.py:61:21
|
59 | def f():
60 | # TODO: Duplicate codes should be counted as duplicate, not unused
61 | # ruff: disable[F841, F841]
| ^^^^
62 | foo = 0
|
help: Remove unused suppression
58 |
59 | def f():
60 | # TODO: Duplicate codes should be counted as duplicate, not unused
- # ruff: disable[F841, F841]
61 + # ruff: disable[F841]
62 | foo = 0
63 |
64 |
RUF100 [*] Unused suppression (unused: `F841`)
--> suppressions.py:69:5
|
67 | # and the other should trigger an unused suppression diagnostic
68 | # ruff: disable[F841]
69 | # ruff: disable[F841]
| ^^^^^^^^^^^^^^^^^^^^^
70 | foo = 0
|
help: Remove unused suppression
66 | # Overlapping range suppressions, one should be marked as used,
67 | # and the other should trigger an unused suppression diagnostic
68 | # ruff: disable[F841]
- # ruff: disable[F841]
69 | foo = 0
70 |
71 |
RUF100 [*] Unused suppression (unused: `E741`)
--> suppressions.py:75:21
|
73 | def f():
74 | # Multiple codes but only one is used
75 | # ruff: disable[E741, F401, F841]
| ^^^^
76 | foo = 0
|
help: Remove unused suppression
72 |
73 | def f():
74 | # Multiple codes but only one is used
- # ruff: disable[E741, F401, F841]
75 + # ruff: disable[F401, F841]
76 | foo = 0
77 |
78 |
RUF100 [*] Unused suppression (non-enabled: `F401`)
--> suppressions.py:75:27
|
73 | def f():
74 | # Multiple codes but only one is used
75 | # ruff: disable[E741, F401, F841]
| ^^^^
76 | foo = 0
|
help: Remove unused suppression
72 |
73 | def f():
74 | # Multiple codes but only one is used
- # ruff: disable[E741, F401, F841]
75 + # ruff: disable[E741, F841]
76 | foo = 0
77 |
78 |
RUF100 [*] Unused suppression (non-enabled: `F401`)
--> suppressions.py:81:27
|
79 | def f():
80 | # Multiple codes but only two are used
81 | # ruff: disable[E741, F401, F841]
| ^^^^
82 | I = 0
|
help: Remove unused suppression
78 |
79 | def f():
80 | # Multiple codes but only two are used
- # ruff: disable[E741, F401, F841]
81 + # ruff: disable[E741, F841]
82 | I = 0
83 |
84 |
RUF100 [*] Unused suppression (unused: `E741`)
--> suppressions.py:87:21
|
85 | def f():
86 | # Multiple codes but none are used
87 | # ruff: disable[E741, F401, F841]
| ^^^^
88 | print("hello")
|
help: Remove unused suppression
84 |
85 | def f():
86 | # Multiple codes but none are used
- # ruff: disable[E741, F401, F841]
87 + # ruff: disable[F401, F841]
88 | print("hello")
RUF100 [*] Unused suppression (non-enabled: `F401`)
--> suppressions.py:87:27
|
85 | def f():
86 | # Multiple codes but none are used
87 | # ruff: disable[E741, F401, F841]
| ^^^^
88 | print("hello")
|
help: Remove unused suppression
84 |
85 | def f():
86 | # Multiple codes but none are used
- # ruff: disable[E741, F401, F841]
87 + # ruff: disable[E741, F841]
88 | print("hello")
RUF100 [*] Unused suppression (unused: `F841`)
--> suppressions.py:87:33
|
85 | def f():
86 | # Multiple codes but none are used
87 | # ruff: disable[E741, F401, F841]
| ^^^^
88 | print("hello")
|
help: Remove unused suppression
84 |
85 | def f():
86 | # Multiple codes but none are used
- # ruff: disable[E741, F401, F841]
87 + # ruff: disable[E741, F401]
88 | print("hello")

View File

@@ -1,8 +1,10 @@
use compact_str::CompactString;
use core::fmt;
use ruff_db::diagnostic::Diagnostic;
use ruff_diagnostics::{Edit, Fix};
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_ast::whitespace::indentation;
use std::cell::Cell;
use std::{error::Error, fmt::Formatter};
use thiserror::Error;
@@ -10,10 +12,14 @@ use ruff_python_trivia::Cursor;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize, TextSlice};
use smallvec::{SmallVec, smallvec};
use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::codes::Rule;
use crate::fix::edits::delete_comment;
use crate::preview::is_range_suppressions_enabled;
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA, UnusedNOQAKind};
use crate::settings::LinterSettings;
#[allow(unused)]
#[derive(Clone, Debug, Eq, PartialEq)]
enum SuppressionAction {
Disable,
@@ -35,7 +41,6 @@ pub(crate) struct SuppressionComment {
reason: TextRange,
}
#[allow(unused)]
impl SuppressionComment {
/// Return the suppressed codes as strings
fn codes_as_str<'src>(&self, source: &'src str) -> impl Iterator<Item = &'src str> {
@@ -52,7 +57,6 @@ pub(crate) struct PendingSuppressionComment<'a> {
comment: SuppressionComment,
}
#[allow(unused)]
impl PendingSuppressionComment<'_> {
/// Whether the comment "matches" another comment, based on indentation and suppressed codes
/// Expects a "forward search" for matches, ie, will only match if the current comment is a
@@ -68,8 +72,7 @@ impl PendingSuppressionComment<'_> {
}
}
#[allow(unused)]
#[derive(Clone, Debug)]
#[derive(Debug)]
pub(crate) struct Suppression {
/// The lint code being suppressed
code: CompactString,
@@ -79,9 +82,11 @@ pub(crate) struct Suppression {
/// Any comments associated with the suppression
comments: SmallVec<[SuppressionComment; 2]>,
/// Whether this suppression actually suppressed a diagnostic
used: Cell<bool>,
}
#[allow(unused)]
#[derive(Copy, Clone, Debug)]
pub(crate) enum InvalidSuppressionKind {
/// Trailing suppression not supported
@@ -114,7 +119,6 @@ pub struct Suppressions {
errors: Vec<ParseError>,
}
#[allow(unused)]
impl Suppressions {
pub fn from_tokens(settings: &LinterSettings, source: &str, tokens: &Tokens) -> Suppressions {
if is_range_suppressions_enabled(settings) {
@@ -147,11 +151,90 @@ impl Suppressions {
for suppression in &self.valid {
if *code == suppression.code.as_str() && suppression.range.contains_range(range) {
suppression.used.set(true);
return true;
}
}
false
}
pub(crate) fn check_suppressions(&self, context: &LintContext, locator: &Locator) {
if !context.any_rule_enabled(&[Rule::UnusedNOQA, Rule::InvalidRuleCode]) {
return;
}
let unused = self
.valid
.iter()
.filter(|suppression| !suppression.used.get());
for suppression in unused {
let Ok(rule) = Rule::from_code(&suppression.code) else {
continue; // TODO: invalid code
};
for comment in &suppression.comments {
let mut range = comment.range;
let edit = if comment.codes.len() == 1 {
delete_comment(comment.range, locator)
} else {
let code_index = comment
.codes
.iter()
.position(|range| locator.slice(range) == suppression.code)
.unwrap();
range = comment.codes[code_index];
let code_range = if code_index < (comment.codes.len() - 1) {
TextRange::new(
comment.codes[code_index].start(),
comment.codes[code_index + 1].start(),
)
} else {
TextRange::new(
comment.codes[code_index - 1].end(),
comment.codes[code_index].end(),
)
};
Edit::range_deletion(code_range)
};
let codes = if context.is_rule_enabled(rule) {
UnusedCodes {
unmatched: vec![suppression.code.to_string()],
..Default::default()
}
} else {
UnusedCodes {
disabled: vec![suppression.code.to_string()],
..Default::default()
}
};
let mut diagnostic = context.report_diagnostic(
UnusedNOQA {
codes: Some(codes),
kind: UnusedNOQAKind::Suppression,
},
range,
);
diagnostic.set_fix(Fix::safe_edit(edit));
}
}
for error in self
.errors
.iter()
.filter(|error| error.kind == ParseErrorKind::MissingCodes)
{
let mut diagnostic = context.report_diagnostic(
UnusedNOQA {
codes: Some(UnusedCodes::default()),
kind: UnusedNOQAKind::Suppression,
},
error.range,
);
diagnostic.set_fix(Fix::safe_edit(delete_comment(error.range, locator)));
}
}
}
#[derive(Default)]
@@ -276,6 +359,7 @@ impl<'a> SuppressionsBuilder<'a> {
code: code.into(),
range: combined_range,
comments: smallvec![comment.comment.clone(), other.comment.clone()],
used: false.into(),
});
}
@@ -292,6 +376,7 @@ impl<'a> SuppressionsBuilder<'a> {
code: code.into(),
range: implicit_range,
comments: smallvec![comment.comment.clone()],
used: false.into(),
});
}
self.pending.remove(comment_index);

View File

@@ -3,13 +3,14 @@ use std::path::Path;
use rustc_hash::FxHashMap;
use ruff_python_trivia::{CommentRanges, SimpleTokenKind, SimpleTokenizer, indentation_at_offset};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer, indentation_at_offset};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::name::{Name, QualifiedName, QualifiedNameBuilder};
use crate::parenthesize::parenthesized_range;
use crate::statement_visitor::StatementVisitor;
use crate::token::Tokens;
use crate::token::parenthesized_range;
use crate::visitor::Visitor;
use crate::{
self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr, ExprNoneLiteral,
@@ -1474,7 +1475,7 @@ pub fn generate_comparison(
ops: &[CmpOp],
comparators: &[Expr],
parent: AnyNodeRef,
comment_ranges: &CommentRanges,
tokens: &Tokens,
source: &str,
) -> String {
let start = left.start();
@@ -1483,8 +1484,7 @@ pub fn generate_comparison(
// Add the left side of the comparison.
contents.push_str(
&source[parenthesized_range(left.into(), parent, comment_ranges, source)
.unwrap_or(left.range())],
&source[parenthesized_range(left.into(), parent, tokens).unwrap_or(left.range())],
);
for (op, comparator) in ops.iter().zip(comparators) {
@@ -1504,7 +1504,7 @@ pub fn generate_comparison(
// Add the right side of the comparison.
contents.push_str(
&source[parenthesized_range(comparator.into(), parent, comment_ranges, source)
&source[parenthesized_range(comparator.into(), parent, tokens)
.unwrap_or(comparator.range())],
);
}

View File

@@ -154,9 +154,7 @@ impl Tokens {
// the tokens which is valid as well.
assert!(
offset >= last.end(),
"Offset {:?} is inside a token range {:?}",
offset,
last.range()
"Offset {offset:?} is inside token `{last:?}`",
);
}
before
@@ -181,9 +179,7 @@ impl Tokens {
// the tokens which is valid as well.
assert!(
offset <= first.start(),
"Offset {:?} is inside a token range {:?}",
offset,
first.range()
"Offset {offset:?} is inside token `{first:?}`",
);
}
@@ -391,7 +387,7 @@ mod tests {
}
#[test]
#[should_panic(expected = "Offset 5 is inside a token range 4..7")]
#[should_panic(expected = "Offset 5 is inside token `Name 4..7`")]
fn tokens_after_offset_inside_token() {
let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter());
tokens.after(TextSize::new(5));
@@ -453,7 +449,7 @@ mod tests {
}
#[test]
#[should_panic(expected = "Offset 5 is inside a token range 4..7")]
#[should_panic(expected = "Offset 5 is inside token `Name 4..7`")]
fn tokens_before_offset_inside_token() {
let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter());
tokens.before(TextSize::new(5));
@@ -505,14 +501,14 @@ mod tests {
}
#[test]
#[should_panic(expected = "Offset 5 is inside a token range 4..7")]
#[should_panic(expected = "Offset 5 is inside token `Name 4..7`")]
fn tokens_in_range_start_offset_inside_token() {
let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter());
tokens.in_range(TextRange::new(5.into(), 10.into()));
}
#[test]
#[should_panic(expected = "Offset 6 is inside a token range 4..7")]
#[should_panic(expected = "Offset 6 is inside token `Name 4..7`")]
fn tokens_in_range_end_offset_inside_token() {
let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter());
tokens.in_range(TextRange::new(0.into(), 6.into()));

View File

@@ -228,3 +228,46 @@ def a():
g = 10
)
(
lambda
* # comment 2
x:
x
)
(
lambda # comment 1
* # comment 2
x:
x
)
(
lambda # comment 1
y,
* # comment 2
x:
x
)
(
lambda
# comment
*x,
**y: x
)
(
lambda
* # comment 2
x,
**y:
x
)
(
lambda
** # comment 1
x:
x
)

View File

@@ -36,7 +36,7 @@ impl Debug for DebugComment<'_> {
}
}
/// Pretty-printed debug representation of [`Comments`].
/// Pretty-printed debug representation of [`Comments`](super::Comments).
pub(crate) struct DebugComments<'a> {
comments: &'a CommentsMap<'a>,
source_code: SourceCode<'a>,

View File

@@ -504,7 +504,7 @@ impl InOrderEntry {
#[derive(Clone, Debug)]
struct OutOfOrderEntry {
/// Index into the [`MultiMap::out_of_order`] vector at which offset the leading vec is stored.
/// Index into the [`MultiMap::out_of_order_parts`] vector at which offset the leading vec is stored.
leading_index: usize,
_count: Count<OutOfOrderEntry>,
}

View File

@@ -2,7 +2,8 @@ use ruff_python_ast::AnyNodeRef;
use std::fmt::{Debug, Formatter};
use std::hash::{Hash, Hasher};
/// Used as key into the [`MultiMap`] storing the comments per node by [`Comments`].
/// Used as key into the [`MultiMap`](super::MultiMap) storing the comments per node by
/// [`Comments`](super::Comments).
///
/// Implements equality and hashing based on the address of the [`AnyNodeRef`] to get fast and cheap
/// hashing/equality comparison.

View File

@@ -871,7 +871,20 @@ fn handle_parameter_comment<'a>(
CommentPlacement::Default(comment)
}
} else if comment.start() < parameter.name.start() {
CommentPlacement::leading(parameter, comment)
// For lambdas, where the parameters cannot be parenthesized and the first parameter thus
// starts at the same position as the parent parameters, mark a comment before the first
// parameter as leading on the parameters rather than the individual parameter to prevent
// the whole parameter list from breaking.
//
// Note that this check is not needed above because lambda parameters cannot have
// annotations.
if let Some(AnyNodeRef::Parameters(parameters)) = comment.enclosing_parent()
&& parameters.start() == parameter.start()
{
CommentPlacement::leading(parameters, comment)
} else {
CommentPlacement::leading(parameter, comment)
}
} else {
CommentPlacement::Default(comment)
}
@@ -1816,7 +1829,7 @@ fn handle_lambda_comment<'a>(
source: &str,
) -> CommentPlacement<'a> {
if let Some(parameters) = lambda.parameters.as_deref() {
// Comments between the `lambda` and the parameters are dangling on the lambda:
// End-of-line comments between the `lambda` and the parameters are dangling on the lambda:
// ```python
// (
// lambda # comment
@@ -1824,8 +1837,22 @@ fn handle_lambda_comment<'a>(
// y
// )
// ```
//
// But own-line comments are leading on the first parameter, if it exists:
// ```python
// (
// lambda
// # comment
// x:
// y
// )
// ```
if comment.start() < parameters.start() {
return CommentPlacement::dangling(comment.enclosing_node(), comment);
return if comment.line_position().is_own_line() {
CommentPlacement::leading(parameters, comment)
} else {
CommentPlacement::dangling(comment.enclosing_node(), comment)
};
}
// Comments between the parameters and the body are dangling on the lambda:
@@ -1947,8 +1974,8 @@ fn handle_unary_op_comment<'a>(
/// )
/// ```
///
/// The comment will be attached to the [`Arguments`] node as a dangling comment, to ensure
/// that it remains on the same line as open parenthesis.
/// The comment will be attached to the [`Arguments`](ast::Arguments) node as a dangling comment, to
/// ensure that it remains on the same line as open parenthesis.
///
/// Similarly, given:
/// ```python
@@ -1957,8 +1984,8 @@ fn handle_unary_op_comment<'a>(
/// ] = ...
/// ```
///
/// The comment will be attached to the [`TypeParams`] node as a dangling comment, to ensure
/// that it remains on the same line as open bracket.
/// The comment will be attached to the [`TypeParams`](ast::TypeParams) node as a dangling comment,
/// to ensure that it remains on the same line as open bracket.
fn handle_bracketed_end_of_line_comment<'a>(
comment: DecoratedComment<'a>,
source: &str,

View File

@@ -174,7 +174,8 @@ impl<'ast> SourceOrderVisitor<'ast> for CommentsVisitor<'ast, '_> {
/// A comment decorated with additional information about its surrounding context in the source document.
///
/// Used by [`CommentStyle::place_comment`] to determine if this should become a [leading](self#leading-comments), [dangling](self#dangling-comments), or [trailing](self#trailing-comments) comment.
/// Used by [`place_comment`] to determine if this should become a [leading](self#leading-comments),
/// [dangling](self#dangling-comments), or [trailing](self#trailing-comments) comment.
#[derive(Debug, Clone)]
pub(crate) struct DecoratedComment<'a> {
enclosing: AnyNodeRef<'a>,
@@ -465,7 +466,7 @@ pub(super) enum CommentPlacement<'a> {
///
/// [`preceding_node`]: DecoratedComment::preceding_node
/// [`following_node`]: DecoratedComment::following_node
/// [`enclosing_node`]: DecoratedComment::enclosing_node_id
/// [`enclosing_node`]: DecoratedComment::enclosing_node
/// [trailing comment]: self#trailing-comments
/// [leading comment]: self#leading-comments
/// [dangling comment]: self#dangling-comments

View File

@@ -166,7 +166,7 @@ impl InterpolatedStringState {
}
}
/// Returns `true` if the interpolated string state is [`NestedInterpolatedElement`].
/// Returns `true` if the interpolated string state is [`Self::NestedInterpolatedElement`].
pub(crate) fn is_nested(self) -> bool {
matches!(self, Self::NestedInterpolatedElement(..))
}

View File

@@ -1095,9 +1095,9 @@ impl OperandIndex {
}
}
/// Returns the index of the operand's right operator. The method always returns an index
/// even if the operand has no right operator. Use [`BinaryCallChain::get_operator`] to test if
/// the operand has a right operator.
/// Returns the index of the operand's right operator. The method always returns an index even
/// if the operand has no right operator. Use [`FlatBinaryExpressionSlice::get_operator`] to
/// test if the operand has a right operator.
fn right_operator(self) -> OperatorIndex {
OperatorIndex::new(self.0 + 1)
}

View File

@@ -32,7 +32,65 @@ impl FormatNodeRule<ExprLambda> for FormatExprLambda {
.split_at(dangling.partition_point(|comment| comment.end() < parameters.start()));
if dangling_before_parameters.is_empty() {
write!(f, [space()])?;
// If the parameters have a leading comment, insert a hard line break. This
// comment is associated as a leading comment on the parameters:
//
// ```py
// (
// lambda
// * # comment
// x:
// x
// )
// ```
//
// so a hard line break is needed to avoid formatting it like:
//
// ```py
// (
// lambda # comment
// *x: x
// )
// ```
//
// which is unstable because it's missing the second space before the comment.
//
// Inserting the line break causes it to format like:
//
// ```py
// (
// lambda
// # comment
// *x :x
// )
// ```
//
// which is also consistent with the formatting in the presence of an actual
// dangling comment on the lambda:
//
// ```py
// (
// lambda # comment 1
// * # comment 2
// x:
// x
// )
// ```
//
// formats to:
//
// ```py
// (
// lambda # comment 1
// # comment 2
// *x: x
// )
// ```
if comments.has_leading(&**parameters) {
hard_line_break().fmt(f)?;
} else {
write!(f, [space()])?;
}
} else {
write!(f, [dangling_comments(dangling_before_parameters)])?;
}

View File

@@ -56,18 +56,20 @@ pub(crate) enum Parenthesize {
/// Adding parentheses is desired to prevent the comments from wandering.
IfRequired,
/// Same as [`Self::IfBreaks`] except that it uses [`parenthesize_if_expands`] for expressions
/// with the layout [`NeedsParentheses::BestFit`] which is used by non-splittable
/// expressions like literals, name, and strings.
/// Same as [`Self::IfBreaks`] except that it uses
/// [`parenthesize_if_expands`](crate::builders::parenthesize_if_expands) for expressions with
/// the layout [`OptionalParentheses::BestFit`] which is used by non-splittable expressions like
/// literals, name, and strings.
///
/// Use this layout over `IfBreaks` when there's a sequence of `maybe_parenthesize_expression`
/// in a single logical-line and you want to break from right-to-left. Use `IfBreaks` for the
/// first expression and `IfBreaksParenthesized` for the rest.
IfBreaksParenthesized,
/// Same as [`Self::IfBreaksParenthesized`] but uses [`parenthesize_if_expands`] for nested
/// [`maybe_parenthesized_expression`] calls unlike other layouts that always omit parentheses
/// when outer parentheses are present.
/// Same as [`Self::IfBreaksParenthesized`] but uses
/// [`parenthesize_if_expands`](crate::builders::parenthesize_if_expands) for nested
/// [`maybe_parenthesized_expression`](crate::expression::maybe_parenthesize_expression) calls
/// unlike other layouts that always omit parentheses when outer parentheses are present.
IfBreaksParenthesizedNested,
}

View File

@@ -214,8 +214,9 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizePattern<'_> {
}
}
/// This function is very similar to [`can_omit_optional_parentheses`] with the only difference that it is for patterns
/// and not expressions.
/// This function is very similar to
/// [`can_omit_optional_parentheses`](crate::expression::can_omit_optional_parentheses)
/// with the only difference that it is for patterns and not expressions.
///
/// The base idea of the omit optional parentheses layout is to prefer using parentheses of sub-patterns
/// when splitting the pattern over introducing new patterns. For example, prefer splitting the sequence pattern in

View File

@@ -72,8 +72,9 @@ impl FormatNodeRule<PatternArguments> for FormatPatternArguments {
}
}
/// Returns `true` if the pattern (which is the only argument to a [`PatternMatchClass`]) is
/// parenthesized. Used to avoid falsely assuming that `x` is parenthesized in cases like:
/// Returns `true` if the pattern (which is the only argument to a
/// [`PatternMatchClass`](ruff_python_ast::PatternMatchClass)) is parenthesized.
/// Used to avoid falsely assuming that `x` is parenthesized in cases like:
/// ```python
/// case Point2D(x): ...
/// ```

View File

@@ -23,7 +23,8 @@ use crate::{FormatModuleError, PyFormatOptions, format_module_source};
///
/// The returned formatted range guarantees to cover at least `range` (excluding whitespace), but the range might be larger.
/// Some cases in which the returned range is larger than `range` are:
/// * The logical lines in `range` use a indentation different from the configured [`IndentStyle`] and [`IndentWidth`].
/// * The logical lines in `range` use a indentation different from the configured [`IndentStyle`]
/// and [`IndentWidth`](ruff_formatter::IndentWidth).
/// * `range` is smaller than a logical lines and the formatter needs to format the entire logical line.
/// * `range` falls on a single line body.
///
@@ -129,16 +130,19 @@ pub fn format_range(
/// b) formatting a sub-expression has fewer split points than formatting the entire expressions.
///
/// ### Possible docstrings
/// Strings that are suspected to be docstrings are excluded from the search to format the enclosing suite instead
/// so that the formatter's docstring detection in [`FormatSuite`] correctly detects and formats the docstrings.
/// Strings that are suspected to be docstrings are excluded from the search to format the enclosing
/// suite instead so that the formatter's docstring detection in
/// [`FormatSuite`](crate::statement::suite::FormatSuite) correctly detects and formats the
/// docstrings.
///
/// ### Compound statements with a simple statement body
/// Don't include simple-statement bodies of compound statements `if True: pass` because the formatter
/// must run [`FormatClauseBody`] to determine if the body should be collapsed or not.
/// must run `FormatClauseBody` to determine if the body should be collapsed or not.
///
/// ### Incorrectly indented code
/// Code that uses indentations that don't match the configured [`IndentStyle`] and [`IndentWidth`] are excluded from the search,
/// because formatting such nodes on their own can lead to indentation mismatch with its sibling nodes.
/// Code that uses indentations that don't match the configured [`IndentStyle`] and
/// [`IndentWidth`](ruff_formatter::IndentWidth) are excluded from the search, because formatting
/// such nodes on their own can lead to indentation mismatch with its sibling nodes.
///
/// ## Suppression comments
/// The search ends when `range` falls into a suppressed range because there's nothing to format. It also avoids that the
@@ -279,13 +283,15 @@ enum EnclosingNode<'a> {
///
/// ## Compound statements with simple statement bodies
/// Similar to [`find_enclosing_node`], exclude the compound statement's body if it is a simple statement (not a suite) from the search to format the entire clause header
/// with the body. This ensures that the formatter runs [`FormatClauseBody`] that determines if the body should be indented.s
/// with the body. This ensures that the formatter runs `FormatClauseBody` that determines if the body should be indented.
///
/// ## Non-standard indentation
/// Node's that use an indentation that doesn't match the configured [`IndentStyle`] and [`IndentWidth`] are excluded from the search.
/// This is because the formatter always uses the configured [`IndentStyle`] and [`IndentWidth`], resulting in the
/// formatted nodes using a different indentation than the unformatted sibling nodes. This would be tolerable
/// in non whitespace sensitive languages like JavaScript but results in lexical errors in Python.
/// Nodes that use an indentation that doesn't match the configured [`IndentStyle`] and
/// [`IndentWidth`](ruff_formatter::IndentWidth) are excluded from the search. This is because the
/// formatter always uses the configured [`IndentStyle`] and
/// [`IndentWidth`](ruff_formatter::IndentWidth), resulting in the formatted nodes using a different
/// indentation than the unformatted sibling nodes. This would be tolerable in non whitespace
/// sensitive languages like JavaScript but results in lexical errors in Python.
///
/// ## Implementation
/// It would probably be possible to merge this visitor with [`FindEnclosingNode`] but they are separate because
@@ -713,9 +719,11 @@ impl Format<PyFormatContext<'_>> for FormatEnclosingNode<'_> {
}
}
/// Computes the level of indentation for `indentation` when using the configured [`IndentStyle`] and [`IndentWidth`].
/// Computes the level of indentation for `indentation` when using the configured [`IndentStyle`]
/// and [`IndentWidth`](ruff_formatter::IndentWidth).
///
/// Returns `None` if the indentation doesn't conform to the configured [`IndentStyle`] and [`IndentWidth`].
/// Returns `None` if the indentation doesn't conform to the configured [`IndentStyle`] and
/// [`IndentWidth`](ruff_formatter::IndentWidth).
///
/// # Panics
/// If `offset` is outside of `source`.

View File

@@ -184,7 +184,7 @@ impl Format<PyFormatContext<'_>> for FormatTargetWithEqualOperator<'_> {
/// No parentheses are added for `short` because it fits into the configured line length, regardless of whether
/// the comment exceeds the line width or not.
///
/// This logic isn't implemented in [`place_comment`] by associating trailing statement comments to the expression because
/// This logic isn't implemented in `place_comment` by associating trailing statement comments to the expression because
/// doing so breaks the suite empty lines formatting that relies on trailing comments to be stored on the statement.
#[derive(Debug)]
pub(super) enum FormatStatementsLastExpression<'a> {
@@ -202,8 +202,8 @@ pub(super) enum FormatStatementsLastExpression<'a> {
/// ] = some_long_value
/// ```
///
/// This layout is preferred over [`RightToLeft`] if the left is unsplittable (single keyword like `return` or a Name)
/// because it has better performance characteristics.
/// This layout is preferred over [`Self::RightToLeft`] if the left is unsplittable (single
/// keyword like `return` or a Name) because it has better performance characteristics.
LeftToRight {
/// The right side of an assignment or the value returned in a return statement.
value: &'a Expr,
@@ -1083,11 +1083,10 @@ impl Format<PyFormatContext<'_>> for InterpolatedString<'_> {
/// For legibility, we discuss only the case of f-strings below, but the
/// same comments apply to t-strings.
///
/// This is just a wrapper around [`FormatFString`] while considering a special
/// case when the f-string is at an assignment statement's value position.
/// This is necessary to prevent an instability where an f-string contains a
/// multiline expression and the f-string fits on the line, but only when it's
/// surrounded by parentheses.
/// This is just a wrapper around [`FormatFString`](crate::other::f_string::FormatFString) while
/// considering a special case when the f-string is at an assignment statement's value position.
/// This is necessary to prevent an instability where an f-string contains a multiline expression
/// and the f-string fits on the line, but only when it's surrounded by parentheses.
///
/// ```python
/// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{

View File

@@ -177,8 +177,10 @@ enum WithItemsLayout<'a> {
/// ...
/// ```
///
/// In this case, use [`maybe_parenthesize_expression`] to format the context expression
/// to get the exact same formatting as when formatting an expression in any other clause header.
/// In this case, use
/// [`maybe_parenthesize_expression`](crate::expression::maybe_parenthesize_expression) to
/// format the context expression to get the exact same formatting as when formatting an
/// expression in any other clause header.
///
/// Only used for Python 3.9+
///

View File

@@ -783,7 +783,7 @@ enum CodeExampleKind<'src> {
///
/// Documentation describing doctests and how they're recognized can be
/// found as part of the Python standard library:
/// https://docs.python.org/3/library/doctest.html.
/// <https://docs.python.org/3/library/doctest.html>.
///
/// (You'll likely need to read the [regex matching] used internally by the
/// doctest module to determine more precisely how it works.)

View File

@@ -38,8 +38,9 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
/// it can't because the string contains the preferred quotes OR
/// it leads to more escaping.
///
/// Note: If you add more cases here where we return `QuoteStyle::Preserve`,
/// make sure to also add them to [`FormatImplicitConcatenatedStringFlat::new`].
/// Note: If you add more cases here where we return `QuoteStyle::Preserve`, make sure to also
/// add them to
/// [`FormatImplicitConcatenatedStringFlat::new`](crate::string::implicit::FormatImplicitConcatenatedStringFlat::new).
pub(super) fn preferred_quote_style(&self, string: StringLikePart) -> QuoteStyle {
let preferred_quote_style = self
.preferred_quote_style

View File

@@ -9,7 +9,7 @@ use crate::prelude::*;
#[derive(Default)]
pub struct FormatTypeParams;
/// Formats a sequence of [`TypeParam`] nodes.
/// Formats a sequence of [`TypeParam`](ruff_python_ast::TypeParam) nodes.
impl FormatNodeRule<TypeParams> for FormatTypeParams {
fn fmt_fields(&self, item: &TypeParams, f: &mut PyFormatter) -> FormatResult<()> {
// A dangling comment indicates a comment on the same line as the opening bracket, e.g.:

View File

@@ -679,8 +679,9 @@ impl Indentation {
/// Returns `true` for a space or tab character.
///
/// This is different than [`is_python_whitespace`] in that it returns `false` for a form feed character.
/// Form feed characters are excluded because they should be preserved in the suppressed output.
/// This is different than [`is_python_whitespace`](ruff_python_trivia::is_python_whitespace) in
/// that it returns `false` for a form feed character. Form feed characters are excluded because
/// they should be preserved in the suppressed output.
const fn is_indent_whitespace(c: char) -> bool {
matches!(c, ' ' | '\t')
}

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