Compare commits

...

126 Commits

Author SHA1 Message Date
Charlie Marsh
cc850ec348 Add box to stmt 2025-12-11 11:24:31 -05: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
Amethyst Reese
4e67a219bb apply range suppressions to filter diagnostics (#21623)
Builds on range suppressions from
https://github.com/astral-sh/ruff/pull/21441

Filters diagnostics based on parsed valid range suppressions.

Issue: #3711
2025-12-08 16:11:59 -08:00
Aria Desires
8ea18966cf [ty] followup: add-import action for reveal_type too (#21668) 2025-12-08 22:44:17 +00:00
Rasmus Nygren
e548ce1ca9 [ty] Enrich function argument auto-complete suggestions with annotated types 2025-12-08 14:19:44 -05:00
Rasmus Nygren
eac8a90cc4 [ty] Add autocomplete suggestions for function arguments
This adds autocomplete suggestions for function arguments. For example,
`okay` in:

```python
def foo(okay=None):

foo(o<CURSOR>
```

This also ensures that we don't suggest a keyword argument if it has
already been used.

Closes astral-sh/issues#1550
2025-12-08 14:19:44 -05:00
Loïc Riegel
2d3466eccf [flake8-bugbear] Accept immutable slice default arguments (B008) (#21823)
Closes issue #21565

## Summary

As pointed out in the issue, slices are currently flagged by B008 but
this behavior is incorrect because slices are immutable.

## Test Plan

Added a test case in the "B006_B008.py" fixture. Sorry for the diff in
the snapshots, the only thing that changes in those flies is the line
numbers, though.

You can also test this manually with this file:
```py
# test_slice.py
def c(d=slice(0, 3)): ...
```

```sh
> target/debug/ruff check tmp/test_slice.py --no-cache --select B008
All checks passed!
```
2025-12-08 14:00:43 -05:00
Phong Do
45fb3732a4 [pydocstyle] Suppress D417 for parameters with Unpack annotations (#21816)
<!--
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

Fixes https://github.com/astral-sh/ruff/issues/8774

This PR fixes `pydocstyle` incorrectly flagging missing argument for
arguments with `Unpack` type annotation by extracting the `kwarg` `D417`
suppression logic into a helper function for future rules as needed.

## Problem Statement

The below example was incorrectly triggering `D417` error for missing
`**kwargs` doc.

```python
class User(TypedDict):
    id: int
    name: str

def do_something(some_arg: str, **kwargs: Unpack[User]):
    """Some doc
    
    Args:
        some_arg: Some argument
    """
```

<img width="1135" height="276" alt="image"
src="https://github.com/user-attachments/assets/42fa4bb9-61a5-4a70-a79c-0c8922a3ee66"
/>

`**kwargs: Unpack[User]` indicates the function expects keyword
arguments that will be unpacked. Ideally, the individual fields of the
User `TypedDict` should be documented, not in the `**kwargs` itself. The
`**kwargs` parameter acts as a semantic grouping rather than a parameter
requiring documentation.

## Solution

As discussed in the linked issue, it makes sense to suppress the `D417`
for parameters with `Unpack` annotation. I extract a helper function to
solely check `D417` should be suppressed with `**kwarg: Unpack[T]`
parameter, this function can also be unit tested independently and
reduce complexity of current `missing_args` check function. This also
makes it easier to add additional rules in the future.

_✏️ Note:_ This is my first PR in this repo, as I've learned a ton from
it, please call out anything that could be improved. Thanks for making
this excellent tool 👏

## Test Plan

Add 2 test cases in `D417.py` and update snapshots.

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-12-08 19:00:05 +00:00
Micha Reiser
0ab8521171 [ty] Remove legacy concise_message fallback behavior (#21847) 2025-12-08 16:19:01 +00:00
Alex Waygood
0ccd84136a [ty] Make Python-version subdiagnostics less verbose (#21849) 2025-12-08 15:58:23 +00:00
Aria Desires
3981a23ee9 [ty] Supress inlay hints when assigning a trivial initializer call (#21848)
## Summary

By taking a purely syntactic approach to the problem of trivial
initializer calls we can supress `x: T = T()`, `x: T = x.y.T()` and `x:
MyNewType = MyNewType(0)` but still display `x: T[U] = T()`.

The place where we drop a ball is this does not compose with our
analysis for supressing `x = (0, "hello")` as `x = (0, T())` and `x =
(T(), T())` will still get inlay hints (I don't think this is a huge
deal).

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

## Test Plan

Existing snapshots cover this well.
2025-12-08 10:54:30 -05:00
Charlie Marsh
385dd2770b [ty] Avoid double-inference on non-tuple argument to Annotated (#21837)
## Summary

If you pass a non-tuple to `Annotated`, we end up running inference on
it twice. I _think_ the only case here is `Annotated[]`, where we insert
a (fake) empty `Name` node in the slice.

Closes https://github.com/astral-sh/ty/issues/1801.
2025-12-08 10:24:05 -05:00
Alex Waygood
7519f6c27b Print Python version and Python platform in the fuzzer output when fuzzing fails (#21844) 2025-12-08 14:35:36 +00:00
David Peter
4686111681 [ty] More SQLAlchemy test updates (#21846)
Minor updates to the SQLAlchemy test suite. I verified all expected
results using pyright.
2025-12-08 15:22:55 +01:00
Micha Reiser
4364ffbdd3 [ty] Don't create a related diagnostic for the primary annotation of sub-diagnostics (#21845) 2025-12-08 14:22:11 +00:00
Charlie Marsh
b845e81c4a Use memchr for computing line indexes (#21838)
## Summary

Some benchmarks with Claude's help:

| File | Size | Baseline | Optimized | Speedup |

|---------------------|-------|----------------------|----------------------|---------|
| numpy/globals.py | 3 KB | 1.48 µs (1.95 GiB/s) | 740 ns (3.89 GiB/s) |
2.0x |
| unicode/pypinyin.py | 4 KB | 2.04 µs (2.01 GiB/s) | 1.18 µs (3.49
GiB/s) | 1.7x |
| pydantic/types.py | 26 KB | 13.1 µs (1.90 GiB/s) | 5.88 µs (4.23
GiB/s) | 2.2x |
| numpy/ctypeslib.py | 17 KB | 8.45 µs (1.92 GiB/s) | 3.94 µs (4.13
GiB/s) | 2.1x |
| large/dataset.py | 41 KB | 21.6 µs (1.84 GiB/s) | 11.2 µs (3.55 GiB/s)
| 1.9x |

I think that I originally thought we _had_ to iterate
character-by-character here because we needed to do the ASCII check, but
the ASCII check can be vectorized by LLVM (and the "search for newlines"
can be done with `memchr`).
2025-12-08 08:50:51 -05:00
David Peter
c99e10eedc [ty] Increase SQLAlchemy test coverage (#21843)
## Summary

Increase our SQLAlchemy test coverage to make sure we understand
`Session.scalar`, `Session.scalars`, `Session.execute` (and their async
equivalents), as well as `Result.tuples`, `Result.one_or_none`,
`Row._tuple`.
2025-12-08 14:36:13 +01:00
Dhruv Manilawala
a364195335 [ty] Avoid diagnostic when typing_extensions.ParamSpec uses default parameter (#21839)
## Summary

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

## Test Plan

Add mdtest.
2025-12-08 12:34:30 +00:00
David Peter
dfd6ed0524 [ty] mdtests with external dependencies (#20904)
## Summary

This PR adds the possibility to write mdtests that specify external
dependencies in a `project` section of TOML blocks. For example, here is
a test that makes sure that we understand Pydantic's dataclass-transform
setup:

````markdown
```toml
[environment]
python-version = "3.12"
python-platform = "linux"

[project]
dependencies = ["pydantic==2.12.2"]
```

```py
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str

user = User(id=1, name="Alice")
reveal_type(user.id)  # revealed: int
reveal_type(user.name)  # revealed: str

# error: [missing-argument] "No argument provided for required parameter
`name`"
invalid_user = User(id=2)
```
````

## How?

Using the `python-version` and the `dependencies` fields from the
Markdown section, we generate a `pyproject.toml` file, write it to a
temporary directory, and use `uv sync` to install the dependencies into
a virtual environment. We then copy the Python source files from that
venv's `site-packages` folder to a corresponding directory structure in
the in-memory filesystem. Finally, we configure the search paths
accordingly, and run the mdtest as usual.

I fully understand that there are valid concerns here:
* Doesn't this require network access? (yes, it does)
* Is this fast enough? (`uv` caching makes this almost unnoticeable,
actually)
* Is this deterministic? ~~(probably not, package resolution can depend
on the platform you're on)~~ (yes, hopefully)

For this reason, this first version is opt-in, locally. ~~We don't even
run these tests in CI (even though they worked fine in a previous
iteration of this PR).~~ You need to set `MDTEST_EXTERNAL=1`, or use the
new `-e/--enable-external` command line option of the `mdtest.py`
runner. For example:
```bash
# Skip mdtests with external dependencies (default):
uv run crates/ty_python_semantic/mdtest.py

# Run all mdtests, including those with external dependencies:
uv run crates/ty_python_semantic/mdtest.py -e

# Only run the `pydantic` tests. Use `-e` to make sure it is not skipped:
uv run crates/ty_python_semantic/mdtest.py -e pydantic
```

## Why?

I believe that this can be a useful addition to our testing strategy,
which lies somewhere between ecosystem tests and normal mdtests.
Ecosystem tests cover much more code, but they have the disadvantage
that we only see second- or third-order effects via diagnostic diffs. If
we unexpectedly gain or lose type coverage somewhere, we might not even
notice (assuming the gradual guarantee holds, and ecosystem code is
mostly correct). Another disadvantage of ecosystem checks is that they
only test checked-in code that is usually correct. However, we also want
to test what happens on wrong code, like the code that is momentarily
written in an editor, before fixing it. On the other end of the spectrum
we have normal mdtests, which have the disadvantage that they do not
reflect the reality of complex real-world code. We experience this
whenever we're surprised by an ecosystem report on a PR.

That said, these tests should not be seen as a replacement for either of
these things. For example, we should still strive to write detailed
self-contained mdtests for user-reported issues. But we might use this
new layer for regression tests, or simply as a debugging tool. It can
also serve as a tool to document our support for popular third-party
libraries.

## Test Plan

* I've been locally using this for a couple of weeks now.
* `uv run crates/ty_python_semantic/mdtest.py -e`
2025-12-08 11:44:20 +01:00
Dhruv Manilawala
ac882f7e63 [ty] Handle various invalid explicit specializations for ParamSpec (#21821)
## Summary

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

## Test Plan

Add new mdtests.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-12-08 05:20:41 +00:00
Alex Waygood
857fd4f683 [ty] Add test case for fixed panic (#21832) 2025-12-07 15:58:11 +00:00
Charlie Marsh
285d6410d3 [ty] Avoid double-analyzing tuple in Final subscript (#21828)
## Summary

As-is, a single-element tuple gets destructured via:

```rust
let arguments = if let ast::Expr::Tuple(tuple) = slice {
    &*tuple.elts
} else {
    std::slice::from_ref(slice)
};
```

But then, because it's a single element, we call
`infer_annotation_expression_impl`, passing in the tuple, rather than
the first element.

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-07 14:27:14 +00:00
Prakhar Pratyush
cbff09b9af [flake8-bandit] Fix false positive when using non-standard CSafeLoader path (S506). (#21830) 2025-12-07 11:40:46 +01:00
Louis Maddox
6e0e49eda8 Add minimal-size build profile (#21826)
This PR adds the same `minimal-size` profile as `uv` repo workspace has

```toml
# Profile to build a minimally sized binary for uv-build
[profile.minimal-size]
inherits = "release"
opt-level = "z"
# This will still show a panic message, we only skip the unwind
panic = "abort"
codegen-units = 1
```
but removes its `panic = "abort"` setting

- As discussed in #21825

Compared to the ones pre-built via `uv tool install`, this builds 35%
smaller ruff and 24% smaller ty binaries
(as measured
[here](https://github.com/lmmx/just-pre-commit/blob/master/refresh_binaries.sh))
2025-12-06 13:19:04 -05:00
Charlie Marsh
ef45c97dab [ty] Allow tuple[Any, ...] to assign to tuple[int, *tuple[int, ...]] (#21803)
## Summary

Closes https://github.com/astral-sh/ty/issues/1750.
2025-12-05 19:04:23 +00:00
Micha Reiser
9714c589e1 [ty] Support renaming import aliases (#21792) 2025-12-05 19:12:13 +01:00
Micha Reiser
b2fb421ddd [ty] Add redeclaration LSP tests (#21812) 2025-12-05 18:02:34 +00:00
Shunsuke Shibayama
2f05ffa2c8 [ty] more detailed description of "Size limit on unions of literals" in mdtest (#21804) 2025-12-05 17:34:39 +00:00
Dhruv Manilawala
b623189560 [ty] Complete support for ParamSpec (#21445)
## Summary

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

This PR adds support for the following capabilities involving a
`ParamSpec` type variable:
- Representing `P.args` and `P.kwargs` in the type system
- Matching against a callable containing `P` to create a type mapping
- Specializing `P` against the stored parameters

The value of a `ParamSpec` type variable is being represented using
`CallableType` with a `CallableTypeKind::ParamSpecValue` variant. This
`CallableTypeKind` is expanded from the existing `is_function_like`
boolean flag. An `enum` is used as these variants are mutually
exclusive.

For context, an initial iteration made an attempt to expand the
`Specialization` to use `TypeOrParameters` enum that represents that a
type variable can specialize into either a `Type` or `Parameters` but
that increased the complexity of the code as all downstream usages would
need to handle both the variants appropriately. Additionally, we'd have
also need to establish an invariant that a regular type variable always
maps to a `Type` while a paramspec type variable always maps to a
`Parameters`.

I've intentionally left out checking and raising diagnostics when the
`ParamSpec` type variable and it's components are not being used
correctly to avoid scope increase and it can easily be done as a
follow-up. This would also include the scoping rules which I don't think
a regular type variable implements either.

## Test Plan

Add new mdtest cases and update existing test cases.

Ran this branch on pyx, no new diagnostics.

### Ecosystem analysis

There's a case where in an annotated assignment like:
```py
type CustomType[P] = Callable[...]

def value[**P](...): ...

def another[**P](...):
	target: CustomType[P] = value
```
The type of `value` is a callable and it has a paramspec that's bound to
`value`, `CustomType` is a type alias that's a callable and `P` that's
used in it's specialization is bound to `another`. Now, ty infers the
type of `target` same as `value` and does not use the declared type
`CustomType[P]`. [This is the
assignment](0980b9d9ab/src/async_utils/gen_transform.py (L108))
that I'm referring to which then leads to error in downstream usage.
Pyright and mypy does seem to use the declared type.

There are multiple diagnostics in `dd-trace-py` that requires support
for `cls`.

I'm seeing `Divergent` type for an example like which ~~I'm not sure
why, I'll look into it tomorrow~~ is because of a cycle as mentioned in
https://github.com/astral-sh/ty/issues/1729#issuecomment-3612279974:
```py
from typing import Callable

def decorator[**P](c: Callable[P, int]) -> Callable[P, str]: ...

@decorator
def func(a: int) -> int: ...

# ((a: int) -> str) | ((a: Divergent) -> str)
reveal_type(func)
```

I ~~need to look into why are the parameters not being specialized
through multiple decorators in the following code~~ think this is also
because of the cycle mentioned in
https://github.com/astral-sh/ty/issues/1729#issuecomment-3612279974 and
the fact that we don't support `staticmethod` properly:
```py
from contextlib import contextmanager

class Foo:
    @staticmethod
    @contextmanager
    def method(x: int):
        yield

foo = Foo()
# ty: Revealed type: `() -> _GeneratorContextManager[Unknown, None, None]` [revealed-type]
reveal_type(foo.method)
```

There's some issue related to `Protocol` that are generic over a
`ParamSpec` in `starlette` which might be related to
https://github.com/astral-sh/ty/issues/1635 but I'm not sure. Here's a
minimal example to reproduce:

<details><summary>Code snippet:</summary>
<p>

```py
from collections.abc import Awaitable, Callable, MutableMapping
from typing import Any, Callable, ParamSpec, Protocol

P = ParamSpec("P")

Scope = MutableMapping[str, Any]
Message = MutableMapping[str, Any]
Receive = Callable[[], Awaitable[Message]]
Send = Callable[[Message], Awaitable[None]]

ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]]

_Scope = Any
_Receive = Callable[[], Awaitable[Any]]
_Send = Callable[[Any], Awaitable[None]]

# Since `starlette.types.ASGIApp` type differs from `ASGIApplication` from `asgiref`
# we need to define a more permissive version of ASGIApp that doesn't cause type errors.
_ASGIApp = Callable[[_Scope, _Receive, _Send], Awaitable[None]]


class _MiddlewareFactory(Protocol[P]):
    def __call__(
        self, app: _ASGIApp, *args: P.args, **kwargs: P.kwargs
    ) -> _ASGIApp: ...


class Middleware:
    def __init__(
        self, factory: _MiddlewareFactory[P], *args: P.args, **kwargs: P.kwargs
    ) -> None:
        self.factory = factory
        self.args = args
        self.kwargs = kwargs


class ServerErrorMiddleware:
    def __init__(
        self,
        app: ASGIApp,
        value: int | None = None,
        flag: bool = False,
    ) -> None:
        self.app = app
        self.value = value
        self.flag = flag

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: ...


# ty: Argument to bound method `__init__` is incorrect: Expected `_MiddlewareFactory[(...)]`, found `<class 'ServerErrorMiddleware'>` [invalid-argument-type]
Middleware(ServerErrorMiddleware, value=500, flag=True)
```

</p>
</details> 

### Conformance analysis

> ```diff
> -constructors_callable.py:36:13: info[revealed-type] Revealed type:
`(...) -> Unknown`
> +constructors_callable.py:36:13: info[revealed-type] Revealed type:
`(x: int) -> Unknown`
> ```

Requires return type inference i.e.,
https://github.com/astral-sh/ruff/pull/21551

> ```diff
> +constructors_callable.py:194:16: error[invalid-argument-type]
Argument is incorrect: Expected `list[T@__init__]`, found `list[Unknown
| str]`
> +constructors_callable.py:194:22: error[invalid-argument-type]
Argument is incorrect: Expected `list[T@__init__]`, found `list[Unknown
| str]`
> +constructors_callable.py:195:4: error[invalid-argument-type] Argument
is incorrect: Expected `list[T@__init__]`, found `list[Unknown | int]`
> +constructors_callable.py:195:9: error[invalid-argument-type] Argument
is incorrect: Expected `list[T@__init__]`, found `list[Unknown | str]`
> ```

I might need to look into why this is happening...

> ```diff
> +generics_defaults.py:79:1: error[type-assertion-failure] Type
`type[Class_ParamSpec[(str, int, /)]]` does not match asserted type
`<class 'Class_ParamSpec'>`
> ```

which is on the following code
```py
DefaultP = ParamSpec("DefaultP", default=[str, int])

class Class_ParamSpec(Generic[DefaultP]): ...

assert_type(Class_ParamSpec, type[Class_ParamSpec[str, int]])
```

It's occurring because there's no equivalence relationship defined
between `ClassLiteral` and `KnownInstanceType::TypeGenericAlias` which
is what these types are.

Everything else looks good to me!
2025-12-05 22:00:06 +05:30
Micha Reiser
f29436ca9e [ty] Update benchmark dependencies (#21815) 2025-12-05 17:23:18 +01:00
Douglas Creager
e42cdf8495 [ty] Carry generic context through when converting class into Callable (#21798)
When converting a class (whether specialized or not) into a `Callable`
type, we should carry through any generic context that the constructor
has. This includes both the generic context of the class itself (if it's
generic) and of the constructor methods (if they are separately
generic).

To help test this, this also updates the `generic_context` extension
function to work on `Callable` types and unions; and adds a new
`into_callable` extension function that works just like
`CallableTypeOf`, but on value forms instead of type forms.

Pulled this out of #21551 for separate review.
2025-12-05 08:57:21 -05:00
Alex Waygood
71a7a03ad4 [ty] Add more tests for renamings (#21810) 2025-12-05 12:41:31 +00:00
Alex Waygood
48f7f42784 [ty] Minor improvements to assert_type diagnostics (#21811) 2025-12-05 12:33:30 +00:00
Micha Reiser
3deb7e1b90 [ty] Add some attribute/method renaming test cases (#21809) 2025-12-05 11:56:28 +01:00
mahiro
5df8a959f5 Update mkdocs-material to 9.7.0 (Insiders now free) (#21797) 2025-12-05 08:53:08 +01:00
Dhruv Manilawala
6f03afe318 Remove unused whitespaces in test cases (#21806)
These aren't used in the tests themselves. There are more instances of
them in other files but those require code changes so I've left them as
it is.
2025-12-05 12:51:40 +05:30
Shunsuke Shibayama
1951f1bbb8 [ty] fix panic when instantiating a type variable with invalid constraints (#21663) 2025-12-04 18:48:38 -08:00
Shunsuke Shibayama
10de342991 [ty] fix build failure caused by conflicts between #21683 and #21800 (#21802) 2025-12-04 18:20:24 -08:00
Shunsuke Shibayama
3511b7a06b [ty] do nothing with store_expression_type if inner_expression_inference_state is Get (#21718)
## Summary

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

## Test Plan

N/A
2025-12-04 18:05:41 -08:00
Shunsuke Shibayama
f3e5713d90 [ty] increase the limit on the number of elements in a non-recursively defined literal union (#21683)
## Summary

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

As explained in https://github.com/astral-sh/ty/issues/957, literal
union types for recursively defined values ​​can be widened early to
speed up the convergence of fixed-point iterations.
This PR achieves this by embedding a marker in `UnionType` that
distinguishes whether a value is recursively defined.

This also allows us to identify values ​​that are not recursively
defined, so I've increased the limit on the number of elements in a
literal union type for such values.

Edit: while this PR doesn't provide the significant performance
improvement initially hoped for, it does have the benefit of allowing
the number of elements in a literal union to be raised above the salsa
limit, and indeed mypy_primer results revealed that a literal union of
220 elements was actually being used.

## Test Plan

`call/union.md` has been updated
2025-12-04 18:01:48 -08:00
Carl Meyer
a9de6b5c3e [ty] normalize typevar bounds/constraints in cycles (#21800)
Fixes https://github.com/astral-sh/ty/issues/1587

## Summary

Perform cycle normalization on typevar bounds and constraints (similar
to how it was already done for typevar defaults) in order to ensure
convergence in cyclic cases.

There might be another fix here that could avoid the cycle in many more
cases, where we don't eagerly evaluate typevar bounds/constraints on
explicit specialization, but just accept the given specialization and
later evaluate to see whether we need to emit a diagnostic on it. But
the current fix here is sufficient to solve the problem and matches the
patterns we use to ensure cycle convergence elsewhere, so it seems good
for now; left a TODO for the other idea.

This fix is sufficient to make us not panic, but not sufficient to get
the semantics fully correct; see the TODOs in the tests. I have ideas
for fixing that as well, but it seems worth at least getting this in to
fix the panic.

## Test Plan

Test that previously panicked now does not.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-12-04 15:17:57 -08:00
Andrew Gallant
06415b1877 [ty] Update completion eval to include modules
Our parsing and confirming of symbol names is highly suspect, but
I think it's fine for now.
2025-12-04 17:37:37 -05:00
Andrew Gallant
518d11b33f [ty] Add modules to auto-import
This makes auto-import include modules in suggestions.

In this initial implementation, we permit this to include submodules as
well. This is in contrast to what we do in `import ...` completions.
It's easy to change this behavior, but I think it'd be interesting to
run with this for now to see how well it works.
2025-12-04 17:37:37 -05:00
Andrew Gallant
da94b99248 [ty] Add support for module-only import requests
The existing importer functionality always required
an import request with a module and a member in that
module. But we want to be able to insert import statements
for a module itself and not any members in the module.

This is basically changing `member: &str` to an
`Option<&str>` and fixing the fallout in a way that
makes sense for module-only imports.
2025-12-04 17:37:37 -05:00
Andrew Gallant
3c2cf49f60 [ty] Refactor auto-import symbol info
This just encapsulates the representation so that
we can make changes to it more easily.
2025-12-04 17:37:37 -05:00
Andrew Gallant
fdcb5a7e73 [ty] Clarify the use of SymbolKind in auto-import 2025-12-04 13:21:26 -05:00
Andrew Gallant
6a025d1925 [ty] Redact ranking of completions from e2e LSP tests
I think changes to this value are generally noise. It's hard to tell
what it means and it isn't especially actionable. We already have an
eval running in CI for completion ranking, so I don't think it's
terribly important to care about ranking here in e2e tests _generally_.
2025-12-04 13:21:26 -05:00
Andrew Gallant
f054e7edf8 [ty] Tweaks tests to use clearer language
A completion lacking a module reference doesn't necessarily mean that
the symbol is defined within the current module. I believe the intent
here is that it means that no import is required to use it.
2025-12-04 13:21:26 -05:00
Andrew Gallant
e154efa229 [ty] Update evaluation results
These are all improvements here with one slight regression on
`reveal_type` ranking. The previous completions offered were:

```
$ cargo r -q -p ty_completion_eval show-one ty-extensions-lower-stdlib
ENOTRECOVERABLE (module: errno)
REG_WHOLE_HIVE_VOLATILE (module: winreg)
SQLITE_NOTICE_RECOVER_WAL (module: _sqlite3)
SupportsGetItemViewable (module: _typeshed)
removeHandler (module: unittest.signals)
reveal_mro (module: ty_extensions)
reveal_protocol_interface (module: ty_extensions)
reveal_type (module: typing) (*, 8/10)
_remove_original_values (module: _osx_support)
_remove_universal_flags (module: _osx_support)
-----
found 10 completions
```

And now they are:

```
$ cargo r -q -p ty_completion_eval show-one ty-extensions-lower-stdlib
ENOTRECOVERABLE (module: errno)
REG_WHOLE_HIVE_VOLATILE (module: winreg)
SQLITE_NOTICE_RECOVER_WAL (module: sqlite3)
SQLITE_NOTICE_RECOVER_WAL (module: sqlite3.dbapi2)
removeHandler (module: unittest)
removeHandler (module: unittest.signals)
reveal_mro (module: ty_extensions)
reveal_protocol_interface (module: ty_extensions)
reveal_type (module: typing) (*, 9/9)
-----
found 9 completions
```

Some completions were removed (because they are now considered
unexported) and some were added (likely do to better re-export support).

This particular case probably warrants more special attention anyway.
So I think this is fine. (It's only a one-ranking regression.)
2025-12-04 13:21:26 -05:00
Andrew Gallant
32f400a457 [ty] Make auto-import ignore symbols in modules starting with a _
This applies recursively. So if *any* component of a module name starts
with a `_`, then symbols from that module are excluded from auto-import.

The exception is when it's a module within first party code. Then we
want to include it in auto-import.
2025-12-04 13:21:26 -05:00
Andrew Gallant
2a38395bc8 [ty] Add some tests for re-exports and __all__ to completions
Note that the `Deprecated` symbols from `importlib.metadata` are no
longer offered because 1) `importlib.metadata` defined `__all__` and 2)
the `Deprecated` symbols aren't in it. These seem to not be a part of
its public API according to the docs, so this seems right to me.
2025-12-04 13:21:26 -05:00
Andrew Gallant
8c72b296c9 [ty] Add support for re-exports and __all__ to auto-import
This commit (mostly) re-implements the support for `__all__` in
ty-proper, but inside the auto-import AST scanner.

When `__all__` isn't present in a module, we fall back to conventions to
determine whether a symbol is exported or not:
https://docs.python.org/3/library/index.html

However, in keeping with current practice for non-auto-import
completions, we continue to provide sunder and dunder names as
re-exports.

When `__all__` is present, we respect it strictly. That is, a symbol is
exported *if and only if* it's in `__all__`. This is somewhat stricter
than pylance seemingly is. I felt like it was a good idea to start here,
and we can relax it based on user demand (perhaps through a setting).
2025-12-04 13:21:26 -05:00
Andrew Gallant
086f1e0b89 [ty] Skip over expressions in auto-import AST scanning 2025-12-04 13:21:26 -05:00
Andrew Gallant
5da45f8ec7 [ty] Simplify auto-import AST visitor slightly and add tests
This simplifies the existing visitor by DRYing it up slightly.
We also add tests for the existing functionality. In particular,
we want to add support for re-export conventions, and that
warrants more careful testing.
2025-12-04 13:21:26 -05:00
Andrew Gallant
62f20b1e86 [ty] Re-arrange imports in symbol extraction
I like using a qualified `ast::` prefix for things from
`ruff_python_ast`, so switch over to that convention.
2025-12-04 13:21:26 -05:00
Aria Desires
cccb0bbaa4 [ty] Add tests for implicit submodule references (#21793)
## Summary

I realized we don't really test `DefinitionKind::ImportFromSubmodule` in
the IDE at all, so here's a bunch of them, just recording our current
behaviour.

## Test Plan

*stares at the camera*
2025-12-04 15:46:23 +00:00
Brent Westbrook
9d4f1c6ae2 Bump 0.14.8 (#21791) 2025-12-04 09:45:53 -05:00
Micha Reiser
326025d45f [ty] Always register rename provider if client doesn't support dynamic registration (#21789) 2025-12-04 14:40:16 +01:00
Micha Reiser
3aefe85b32 [ty] Ensure rename CursorTest calls can_rename before renaming (#21790) 2025-12-04 14:19:48 +01:00
Dhruv Manilawala
b8ecc83a54 Fix clippy errors on main (#21788)
https://github.com/astral-sh/ruff/actions/runs/19922070773/job/57112827024#step:5:62
2025-12-04 16:20:37 +05:30
Aria Desires
6491932757 [ty] Fix crash when hovering an unknown string annotation (#21782)
## Summary

I have no idea what I'm doing with the fix (all the interesting stuff is
in the second commit).

The basic problem is the compiler emits the diagnostic:

```
x: "foobar"
    ^^^^^^
```

Which the suppression code-action hands the end of to `Tokens::after`
which then panics because that function panics if handed an offset that
is in the middle of a token.

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

## Test Plan

Many tests added (only the e2e test matters).
2025-12-04 09:11:40 +01:00
Micha Reiser
a9f2bb41bd [ty] Don't send publish diagnostics for clients supporting pull diagnostics (#21772) 2025-12-04 08:12:04 +01:00
Aria Desires
e2b72fbf99 [ty] cleanup test path (#21781)
Fixes
https://github.com/astral-sh/ruff/pull/21745#discussion_r2586552295
2025-12-03 21:54:50 +00:00
Alex Waygood
14fce0d440 [ty] Improve the display of various special-form types (#21775) 2025-12-03 21:19:59 +00:00
Alex Waygood
8ebecb2a88 [ty] Add subdiagnostic hint if the user wrote X = Any rather than X: Any (#21777) 2025-12-03 20:42:21 +00:00
Aria Desires
45ac30a4d7 [ty] Teach ty the meaning of desperation (try ancestor pyproject.tomls as search-paths if module resolution fails) (#21745)
## Summary

This makes an importing file a required argument to module resolution,
and if the fast-path cached query fails to resolve the module, take the
slow-path uncached (could be cached if we want)
`desperately_resolve_module` which will walk up from the importing file
until it finds a `pyproject.toml` (arbitrary decision, we could try
every ancestor directory), at which point it takes one last desperate
attempt to use that directory as a search-path. We do not continue
walking up once we've found a `pyproject.toml` (arbitrary decision, we
could keep going up).

Running locally, this fixes every broken-for-workspace-reasons import in
pyx's workspace!

* Fixes https://github.com/astral-sh/ty/issues/1539
* Improves https://github.com/astral-sh/ty/issues/839

## Test Plan

The workspace tests see a huge improvement on most absolute imports.
2025-12-03 15:04:36 -05:00
Alex Waygood
0280949000 [ty] fix panic when attempting to infer the variance of a PEP-695 class that depends on a recursive type aliases and also somehow protocols (#21778)
Fixes https://github.com/astral-sh/ty/issues/1716.

## Test plan

I added a corpus snippet that causes us to panic on `main` (I tested by
running `cargo run -p ty_python_semantic --test=corpus` without the fix
applied).
2025-12-03 19:01:42 +00:00
Bhuminjay Soni
c722f498fe [flake8-bugbear] Catch yield expressions within other statements (B901) (#21200)
## Summary

This PR re-implements [return-in-generator
(B901)](https://docs.astral.sh/ruff/rules/return-in-generator/#return-in-generator-b901)
for async generators as a semantic syntax error. This is not a syntax
error for sync generators, so we'll need to preserve both the lint rule
and the syntax error in this case.

It also updates B901 and the new implementation to catch cases where the
generator's `yield` or `yield from` expression is part of another
statement, as in:

```py
def foo():
    return (yield)
```

These were previously not caught because we only looked for
`Stmt::Expr(Expr::Yield)` in `visit_stmt` instead of visiting `yield`
expressions directly. I think this modification is within the spirit of
the rule and safe to try out since the rule is in preview.

## Test Plan

<!-- How was it tested? -->
I have written tests as directed in #17412

---------

Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <bhuminjaysoni@gmail.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-12-03 12:05:15 -05:00
David Peter
1f4f8d9950 [ty] Fix flow of associated member states during star imports (#21776)
## Summary

Star-imports can not just affect the state of symbols that they pull in,
they can also affect the state of members that are associated with those
symbols. For example, if `obj.attr` was previously narrowed from `int |
None` to `int`, and a star-import now overwrites `obj`, then the
narrowing on `obj.attr` should be "reset".

This PR keeps track of the state of associated members during star
imports and properly models the flow of their corresponding state
through the control flow structure that we artificially create for
star-imports.

See [this
comment](https://github.com/astral-sh/ty/issues/1355#issuecomment-3607125005)
for an explanation why this caused ty to see certain `asyncio` symbols
as not being accessible on Python 3.14.

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

## Ecosystem impact

```diff
async-utils (https://github.com/mikeshardmind/async-utils)
- src/async_utils/bg_loop.py:115:31: error[invalid-argument-type] Argument to bound method `set_task_factory` is incorrect: Expected `_TaskFactory | None`, found `def eager_task_factory[_T_co](loop: AbstractEventLoop | None, coro: Coroutine[Any, Any, _T_co@eager_task_factory], *, name: str | None = None, context: Context | None = None) -> Task[_T_co@eager_task_factory]`
- Found 30 diagnostics
+ Found 29 diagnostics

mitmproxy (https://github.com/mitmproxy/mitmproxy)
+ mitmproxy/utils/asyncio_utils.py:96:60: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- test/conftest.py:37:31: error[invalid-argument-type] Argument to bound method `set_task_factory` is incorrect: Expected `_TaskFactory | None`, found `def eager_task_factory[_T_co](loop: AbstractEventLoop | None, coro: Coroutine[Any, Any, _T_co@eager_task_factory], *, name: str | None = None, context: Context | None = None) -> Task[_T_co@eager_task_factory]`
```

All of these seem to be correct, they give us a different type for
`asyncio` symbols that are now imported from different
`sys.version_info` branches (where we previously failed to recognize
some of these as statically true/false).

```diff
dd-trace-py (https://github.com/DataDog/dd-trace-py)
- ddtrace/contrib/internal/asyncio/patch.py:39:12: error[invalid-argument-type] Argument to function `unwrap` is incorrect: Expected `WrappedFunction`, found `def create_task[_T](self, coro: Coroutine[Any, Any, _T@create_task] | Generator[Any, None, _T@create_task], *, name: object = None) -> Task[_T@create_task]`
+ ddtrace/contrib/internal/asyncio/patch.py:39:12: error[invalid-argument-type] Argument to function `unwrap` is incorrect: Expected `WrappedFunction`, found `def create_task[_T](self, coro: Generator[Any, None, _T@create_task] | Coroutine[Any, Any, _T@create_task], *, name: object = None) -> Task[_T@create_task]`
```

Similar, but only results in a diagnostic change.

## Test Plan

Added a regression test
2025-12-03 17:52:31 +01:00
William Woodruff
4488e9d47d Revert "Enable PEP 740 attestations when publishing to PyPI" (#21768) 2025-12-03 11:07:29 -05:00
github-actions[bot]
b08f0b2caa [ty] Sync vendored typeshed stubs (#21715)
Co-authored-by: typeshedbot <>
Co-authored-by: David Peter <mail@david-peter.de>
2025-12-03 15:49:51 +00:00
David Peter
d6e472f297 [ty] Reachability constraints: minor documentation fixes (#21774) 2025-12-03 16:40:11 +01:00
Douglas Creager
45842cc034 [ty] Fix non-determinism in ConstraintSet.specialize_constrained (#21744)
This fixes a non-determinism that we were seeing in the constraint set
tests in https://github.com/astral-sh/ruff/pull/21715.

In this test, we create the following constraint set, and then try to
create a specialization from it:

```
(T@constrained_by_gradual_list = list[Base])
  ∨
(Bottom[list[Any]] ≤ T@constrained_by_gradual_list ≤ Top[list[Any]])
```

That is, `T` is either specifically `list[Base]`, or it's any `list`.
Our current heuristics say that, absent other restrictions, we should
specialize `T` to the more specific type (`list[Base]`).

In the correct test output, we end up creating a BDD that looks like
this:

```
(T@constrained_by_gradual_list = list[Base])
┡━₁ always
└─₀ (Bottom[list[Any]] ≤ T@constrained_by_gradual_list ≤ Top[list[Any]])
    ┡━₁ always
    └─₀ never
```

In the incorrect output, the BDD looks like this:

```
(Bottom[list[Any]] ≤ T@constrained_by_gradual_list ≤ Top[list[Any]])
┡━₁ always
└─₀ never
```

The difference is the ordering of the two individual constraints. Both
constraints appear in the first BDD, but the second BDD only contains `T
is any list`. If we were to force the second BDD to contain both
constraints, it would look like this:

```
(Bottom[list[Any]] ≤ T@constrained_by_gradual_list ≤ Top[list[Any]])
┡━₁ always
└─₀ (T@constrained_by_gradual_list = list[Base])
    ┡━₁ always
    └─₀ never
```

This is the standard shape for an OR of two constraints. However! Those
two constraints are not independent of each other! If `T` is
specifically `list[Base]`, then it's definitely also "any `list`". From
that, we can infer the contrapositive: that if `T` is not any list, then
it cannot be `list[Base]` specifically. When we encounter impossible
situations like that, we prune that path in the BDD, and treat it as
`false`. That rewrites the second BDD to the following:

```
(Bottom[list[Any]] ≤ T@constrained_by_gradual_list ≤ Top[list[Any]])
┡━₁ always
└─₀ (T@constrained_by_gradual_list = list[Base])
    ┡━₁ never   <-- IMPOSSIBLE, rewritten to never
    └─₀ never
```

We then would see that that BDD node is redundant, since both of its
outgoing edges point at the `never` node. Our BDDs are _reduced_, which
means we have to remove that redundant node, resulting in the BDD we saw
above:

```
(Bottom[list[Any]] ≤ T@constrained_by_gradual_list ≤ Top[list[Any]])
┡━₁ always
└─₀ never       <-- redundant node removed
```

The end result is that we were "forgetting" about the `T = list[Base]`
constraint, but only for some BDD variable orderings.

To fix this, I'm leaning in to the fact that our BDDs really do need to
"remember" all of the constraints that they were created with. Some
combinations might not be possible, but we now have the sequent map,
which is quite good at detecting and pruning those.

So now our BDDs are _quasi-reduced_, which just means that redundant
nodes are allowed. (At first I was worried that allowing redundant nodes
would be an unsound "fix the glitch". But it turns out they're real!
[This](https://ieeexplore.ieee.org/abstract/document/130209) is the
paper that introduces them, though it's very difficult to read. Knuth
mentions them in §7.1.4 of
[TAOCP](https://course.khoury.northeastern.edu/csu690/ssl/bdd-knuth.pdf),
and [this paper](https://par.nsf.gov/servlets/purl/10128966) has a nice
short summary of them in §2.)

While we're here, I've added a bunch of `debug` and `trace` level log
messages to the constraint set implementation. I was getting tired of
having to add these by hands over and over. To enable them, just set
`TY_LOG` in your environment, e.g.

```sh
env TY_LOG=ty_python_semantic::types::constraints::SequentMap=trace ty check ...
```

[Note, this has an `internal` label because are still not using
`specialize_constrained` in anything user-facing yet.]
2025-12-03 10:19:39 -05:00
Alex Waygood
cd079bd92e [ty] Improve @override, @final and Liskov checks in cases where there are multiple reachable definitions (#21767) 2025-12-03 12:51:36 +00:00
Alex Waygood
5756b3809c [ty] Extend invalid-explicit-override to also cover properties decorated with @override that do not override anything (#21756) 2025-12-03 11:27:47 +00:00
Micha Reiser
92c5f62ec0 [ty] Enable LRU collection for parsed module (#21749) 2025-12-03 12:16:18 +01:00
David Peter
21e5a57296 [ty] Support typevar-specialized dynamic types in generic type aliases (#21730)
## Summary

For a type alias like the one below, where `UnknownClass` is something
with a dynamic type, we previously lost track of the fact that this
dynamic type was explicitly specialized *with a type variable*. If that
alias is then later explicitly specialized itself (`MyAlias[int]`), we
would miscount the number of legacy type variables and emit a
`invalid-type-arguments` diagnostic
([playground](https://play.ty.dev/886ae6cc-86c3-4304-a365-510d29211f85)).
```py
T = TypeVar("T")

MyAlias: TypeAlias = UnknownClass[T] | None
```
The solution implemented here is not pretty, but we can hopefully get
rid of it via https://github.com/astral-sh/ty/issues/1711. Also, once we
properly support `ParamSpec` and `Concatenate`, we should be able to
remove some of this code.

This addresses many of the `invalid-type-arguments` false-positives in
https://github.com/astral-sh/ty/issues/1685. With this change, there are
still some diagnostics of this type left. Instead of implementing even
more (rather sophisticated) workarounds for these cases as well, it
might be much easier to wait for full `ParamSpec`/`Concatenate` support
and then try again.

A disadvantage of this implementation is that we lose track of some
`@Todo` types and replace them with `Unknown`. We could spend more
effort and try to preserve them, but I'm unsure if this is the best use
of our time right now.

## Test Plan

New Markdown tests.
2025-12-03 10:00:02 +01:00
Denys Zhak
f4e4229683 Add token based parenthesized_ranges implementation (#21738)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-12-03 08:15:17 +00:00
David Peter
e6ddeed386 [ty] Default-specialization of generic type aliases (#21765)
## Summary

Implement default-specialization of generic type aliases (implicit or
PEP-613) if they are used in a type expression without an explicit
specialization.

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

## Typing conformance

```diff
-generics_defaults_specialization.py:26:5: error[type-assertion-failure] Type `SomethingWithNoDefaults[int, str]` does not match asserted type `SomethingWithNoDefaults[int, DefaultStrT]`
```

That's exactly what we want ✔️ 

All other tests in this file pass as well, with the exception of this
assertion, which is just wrong (at least according to our
interpretation, `type[Bar] != <class 'Bar'>`). I checked that we do
correctly default-specialize the type parameter which is not displayed
in the diagnostic that we raise.
```py
class Bar(SubclassMe[int, DefaultStrT]): ...

assert_type(Bar, type[Bar[str]])  # ty: Type `type[Bar[str]]` does not match asserted type `<class 'Bar'>`
```

## Ecosystem impact

Looks like I should have included this last week 😎 

## Test Plan

Updated pre-existing tests and add a few new ones.
2025-12-03 09:10:45 +01:00
Alex Waygood
c5b8d551df [ty] Suppress false positives when dataclasses.dataclass(...)(cls) is called imperatively (#21729)
Fixes https://github.com/astral-sh/ty/issues/1705
2025-12-03 08:05:25 +00:00
Bhuminjay Soni
f68080b55e [syntax-error] Default type parameter followed by non-default type parameter (#21657)
## Summary

This PR implements syntax error where a default type parameter is
followed by a non-default type parameter.
https://github.com/astral-sh/ruff/issues/17412#issuecomment-3584088217


## Test Plan

I have written inline tests as directed in #17412

---------

Signed-off-by: 11happy <bhuminjaysoni@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
2025-12-03 12:01:31 +05:30
Amethyst Reese
abaa49f552 new module for parsing ranged suppressions (#21441)
This adds a new `suppression` module to the `ruff_linter` crate, similar
to the suppression
module for ty, to parse comments for ruff suppression directives, such
as `# ruff: disable[CODE]`.
2025-12-02 15:39:59 -08:00
Ibraheem Ahmed
7b0aab1696 [ty] type[T] is assignable to an inferable typevar (#21766)
## Summary

Resolves https://github.com/astral-sh/ty/issues/1712.
2025-12-02 18:25:09 -05:00
Brent Westbrook
2250fa6f98 Fix syntax error false positives for await outside functions (#21763)
## Summary

Fixes #21750 and a related bug in `PLE1142`. We were not properly
considering generators to be valid `await` contexts, which caused the
`F704` issue. One of the tests I added for this also uncovered an issue
in `PLE1142` for comprehensions nested within async generators because
we were only checking the current scope rather than traversing the
nested context.

## Test Plan

Both of these rules are implemented as semantic syntax errors, so I
added tests (and fixes) in both Ruff and ty.
2025-12-02 21:02:02 +00:00
Alex Waygood
392a8e4e50 [ty] Improve diagnostics for unsupported comparison operations (#21737) 2025-12-02 19:58:45 +00:00
Micha Reiser
515de2d062 Move Token, TokenKind and Tokens to ruff-python-ast (#21760) 2025-12-02 20:10:46 +01:00
613 changed files with 36949 additions and 18376 deletions

View File

@@ -75,14 +75,6 @@
matchManagers: ["cargo"],
enabled: false,
},
{
// `mkdocs-material` requires a manual update to keep the version in sync
// with `mkdocs-material-insider`.
// See: https://squidfunk.github.io/mkdocs-material/insiders/upgrade/
matchManagers: ["pip_requirements"],
matchPackageNames: ["mkdocs-material"],
enabled: false,
},
{
groupName: "pre-commit dependencies",
matchManagers: ["pre-commit"],

View File

@@ -24,6 +24,8 @@ env:
PACKAGE_NAME: ruff
PYTHON_VERSION: "3.14"
NEXTEST_PROFILE: ci
# Enable mdtests that require external dependencies
MDTEST_EXTERNAL: "1"
jobs:
determine_changes:
@@ -296,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"
@@ -779,8 +781,6 @@ jobs:
name: "mkdocs"
runs-on: ubuntu-latest
timeout-minutes: 10
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
@@ -788,11 +788,6 @@ jobs:
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
with:
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
@@ -800,11 +795,7 @@ jobs:
with:
python-version: 3.13
activate-environment: true
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt
- name: "Install dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: uv pip install -r docs/requirements.txt
- name: "Update README File"
run: python scripts/transform_readme.py --target mkdocs
@@ -812,12 +803,8 @@ jobs:
run: python scripts/generate_mkdocs.py
- name: "Check docs formatting"
run: python scripts/check_docs_formatted.py
- name: "Build Insiders docs"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: mkdocs build --strict -f mkdocs.insiders.yml
- name: "Build docs"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: mkdocs build --strict -f mkdocs.public.yml
run: mkdocs build --strict -f mkdocs.yml
check-formatter-instability-and-black-similarity:
name: "formatter instabilities and black similarity"

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

View File

@@ -20,8 +20,6 @@ on:
jobs:
mkdocs:
runs-on: ubuntu-latest
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
@@ -59,23 +57,12 @@ jobs:
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> "$GITHUB_ENV"
echo "timestamp=$timestamp" >> "$GITHUB_ENV"
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
with:
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: pip install -r docs/requirements-insiders.txt
- name: "Install dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: pip install -r docs/requirements.txt
- name: "Copy README File"
@@ -83,13 +70,8 @@ jobs:
python scripts/transform_readme.py --target mkdocs
python scripts/generate_mkdocs.py
- name: "Build Insiders docs"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: mkdocs build --strict -f mkdocs.insiders.yml
- name: "Build docs"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: mkdocs build --strict -f mkdocs.public.yml
run: mkdocs build --strict -f mkdocs.yml
- name: "Clone docs repo"
run: git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs

View File

@@ -18,7 +18,8 @@ jobs:
environment:
name: release
permissions:
id-token: write # For PyPI's trusted publishing + PEP 740 attestations
# For PyPI's trusted publishing.
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
@@ -27,8 +28,5 @@ jobs:
pattern: wheels-*
path: wheels
merge-multiple: true
- uses: astral-sh/attest-action@2c727738cea36d6c97dd85eb133ea0e0e8fe754b # v0.0.4
with:
paths: wheels/*
- name: Publish to PyPi
run: uv publish -v wheels/*

View File

@@ -1,5 +1,34 @@
# Changelog
## 0.14.8
Released on 2025-12-04.
### Preview features
- \[`flake8-bugbear`\] Catch `yield` expressions within other statements (`B901`) ([#21200](https://github.com/astral-sh/ruff/pull/21200))
- \[`flake8-use-pathlib`\] Mark fixes unsafe for return type changes (`PTH104`, `PTH105`, `PTH109`, `PTH115`) ([#21440](https://github.com/astral-sh/ruff/pull/21440))
### Bug fixes
- Fix syntax error false positives for `await` outside functions ([#21763](https://github.com/astral-sh/ruff/pull/21763))
- \[`flake8-simplify`\] Fix truthiness assumption for non-iterable arguments in tuple/list/set calls (`SIM222`, `SIM223`) ([#21479](https://github.com/astral-sh/ruff/pull/21479))
### Documentation
- Suggest using `--output-file` option in GitLab integration ([#21706](https://github.com/astral-sh/ruff/pull/21706))
### Other changes
- [syntax-error] Default type parameter followed by non-default type parameter ([#21657](https://github.com/astral-sh/ruff/pull/21657))
### Contributors
- [@kieran-ryan](https://github.com/kieran-ryan)
- [@11happy](https://github.com/11happy)
- [@danparizher](https://github.com/danparizher)
- [@ntBre](https://github.com/ntBre)
## 0.14.7
Released on 2025-11-28.

View File

@@ -331,13 +331,6 @@ you addressed them.
## MkDocs
> [!NOTE]
>
> The documentation uses Material for MkDocs Insiders, which is closed-source software.
> This means only members of the Astral organization can preview the documentation exactly as it
> will appear in production.
> Outside contributors can still preview the documentation, but there will be some differences. Consult [the Material for MkDocs documentation](https://squidfunk.github.io/mkdocs-material/insiders/benefits/#features) for which features are exclusively available in the insiders version.
To preview any changes to the documentation locally:
1. Install the [Rust toolchain](https://www.rust-lang.org/tools/install).
@@ -351,11 +344,7 @@ To preview any changes to the documentation locally:
1. Run the development server with:
```shell
# For contributors.
uvx --with-requirements docs/requirements.txt -- mkdocs serve -f mkdocs.public.yml
# For members of the Astral org, which has access to MkDocs Insiders via sponsorship.
uvx --with-requirements docs/requirements-insiders.txt -- mkdocs serve -f mkdocs.insiders.yml
uvx --with-requirements docs/requirements.txt -- mkdocs serve -f mkdocs.yml
```
The documentation should then be available locally at

41
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",
@@ -2859,7 +2860,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.7"
version = "0.14.8"
dependencies = [
"anyhow",
"argfile",
@@ -3117,13 +3118,14 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.7"
version = "0.14.8"
dependencies = [
"aho-corasick",
"anyhow",
"bitflags 2.10.0",
"clap",
"colored 3.0.0",
"compact_str",
"fern",
"glob",
"globset",
@@ -3347,6 +3349,7 @@ dependencies = [
"compact_str",
"get-size2",
"insta",
"itertools 0.14.0",
"memchr",
"ruff_annotate_snippets",
"ruff_python_ast",
@@ -3472,7 +3475,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.7"
version = "0.14.8"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3570,7 +3573,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -3588,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",
@@ -3599,6 +3602,7 @@ dependencies = [
"indexmap",
"intrusive-collections",
"inventory",
"ordermap",
"parking_lot",
"portable-atomic",
"rustc-hash",
@@ -3612,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",
@@ -3971,7 +3975,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -4556,6 +4560,7 @@ dependencies = [
"anyhow",
"camino",
"colored 3.0.0",
"dunce",
"insta",
"memchr",
"path-slash",
@@ -5024,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",
@@ -272,6 +272,12 @@ large_stack_arrays = "allow"
lto = "fat"
codegen-units = 16
# Profile to build a minimally sized binary for ruff/ty
[profile.minimal-size]
inherits = "release"
opt-level = "z"
codegen-units = 1
# Some crates don't change as much but benefit more from
# more expensive optimization passes, so we selectively
# decrease codegen-units in some cases.

View File

@@ -147,8 +147,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.7/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.8/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.8/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -181,7 +181,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.7
rev: v0.14.8
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.14.7"
version = "0.14.8"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1440,6 +1440,78 @@ def function():
Ok(())
}
#[test]
fn ignore_noqa() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"ruff.toml",
r#"
[lint]
select = ["F401"]
"#,
)?;
fixture.write_file(
"noqa.py",
r#"
import os # noqa: F401
# ruff: disable[F401]
import sys
"#,
)?;
// without --ignore-noqa
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py"),
@r"
success: false
exit_code: 1
----- stdout -----
noqa.py:5:8: F401 [*] `sys` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
");
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.args(["--preview"]),
@r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
");
// with --ignore-noqa --preview
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.args(["--ignore-noqa", "--preview"]),
@r"
success: false
exit_code: 1
----- stdout -----
noqa.py:2:8: F401 [*] `os` imported but unused
noqa.py:5:8: F401 [*] `sys` imported but unused
Found 2 errors.
[*] 2 fixable with the `--fix` option.
----- stderr -----
");
Ok(())
}
#[test]
fn add_noqa() -> Result<()> {
let fixture = CliTest::new()?;
@@ -1632,6 +1704,100 @@ def unused(x): # noqa: ANN001, ARG001, D103
Ok(())
}
#[test]
fn add_noqa_existing_file_level_noqa() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"ruff.toml",
r#"
[lint]
select = ["F401"]
"#,
)?;
fixture.write_file(
"noqa.py",
r#"
# ruff: noqa F401
import os
"#,
)?;
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.arg("--preview")
.args(["--add-noqa"])
.arg("-")
.pass_stdin(r#"
"#), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
let test_code =
fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
insta::assert_snapshot!(test_code, @r"
# ruff: noqa F401
import os
");
Ok(())
}
#[test]
fn add_noqa_existing_range_suppression() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"ruff.toml",
r#"
[lint]
select = ["F401"]
"#,
)?;
fixture.write_file(
"noqa.py",
r#"
# ruff: disable[F401]
import os
"#,
)?;
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.arg("--preview")
.args(["--add-noqa"])
.arg("-")
.pass_stdin(r#"
"#), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
let test_code =
fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
insta::assert_snapshot!(test_code, @r"
# ruff: disable[F401]
import os
");
Ok(())
}
#[test]
fn add_noqa_multiline_comment() -> Result<()> {
let fixture = CliTest::new()?;

View File

@@ -6,7 +6,8 @@ use criterion::{
use ruff_benchmark::{
LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, TestCase, UNICODE_PYPINYIN,
};
use ruff_python_parser::{Mode, TokenKind, lexer};
use ruff_python_ast::token::TokenKind;
use ruff_python_parser::{Mode, lexer};
#[cfg(target_os = "windows")]
#[global_allocator]

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

@@ -166,28 +166,8 @@ impl Diagnostic {
/// Returns the primary message for this diagnostic.
///
/// A diagnostic always has a message, but it may be empty.
///
/// NOTE: At present, this routine will return the first primary
/// annotation's message as the primary message when the main diagnostic
/// message is empty. This is meant to facilitate an incremental migration
/// in ty over to the new diagnostic data model. (The old data model
/// didn't distinguish between messages on the entire diagnostic and
/// messages attached to a particular span.)
pub fn primary_message(&self) -> &str {
if !self.inner.message.as_str().is_empty() {
return self.inner.message.as_str();
}
// FIXME: As a special case, while we're migrating ty
// to the new diagnostic data model, we'll look for a primary
// message from the primary annotation. This is because most
// ty diagnostics are created with an empty diagnostic
// message and instead attach the message to the annotation.
// Fixing this will require touching basically every diagnostic
// in ty, so we do it this way for now to match the old
// semantics. ---AG
self.primary_annotation()
.and_then(|ann| ann.get_message())
.unwrap_or_default()
self.inner.message.as_str()
}
/// Introspects this diagnostic and returns what kind of "primary" message
@@ -199,18 +179,6 @@ impl Diagnostic {
/// contains *essential* information or context for understanding the
/// diagnostic.
///
/// The reason why we don't just always return both the main diagnostic
/// message and the primary annotation message is because this was written
/// in the midst of an incremental migration of ty over to the new
/// diagnostic data model. At time of writing, diagnostics were still
/// constructed in the old model where the main diagnostic message and the
/// primary annotation message were not distinguished from each other. So
/// for now, we carefully return what kind of messages this diagnostic
/// contains. In effect, if this diagnostic has a non-empty main message
/// *and* a non-empty primary annotation message, then the diagnostic is
/// 100% using the new diagnostic data model and we can format things
/// appropriately.
///
/// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what
/// you want.
@@ -224,11 +192,10 @@ impl Diagnostic {
.primary_annotation()
.and_then(|ann| ann.get_message())
.unwrap_or_default();
match (main.is_empty(), annotation.is_empty()) {
(false, true) => ConciseMessage::MainDiagnostic(main),
(true, false) => ConciseMessage::PrimaryAnnotation(annotation),
(false, false) => ConciseMessage::Both { main, annotation },
(true, true) => ConciseMessage::Empty,
if annotation.is_empty() {
ConciseMessage::MainDiagnostic(main)
} else {
ConciseMessage::Both { main, annotation }
}
}
@@ -693,18 +660,6 @@ impl SubDiagnostic {
/// contains *essential* information or context for understanding the
/// diagnostic.
///
/// The reason why we don't just always return both the main diagnostic
/// message and the primary annotation message is because this was written
/// in the midst of an incremental migration of ty over to the new
/// diagnostic data model. At time of writing, diagnostics were still
/// constructed in the old model where the main diagnostic message and the
/// primary annotation message were not distinguished from each other. So
/// for now, we carefully return what kind of messages this diagnostic
/// contains. In effect, if this diagnostic has a non-empty main message
/// *and* a non-empty primary annotation message, then the diagnostic is
/// 100% using the new diagnostic data model and we can format things
/// appropriately.
///
/// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what
/// you want.
@@ -714,11 +669,10 @@ impl SubDiagnostic {
.primary_annotation()
.and_then(|ann| ann.get_message())
.unwrap_or_default();
match (main.is_empty(), annotation.is_empty()) {
(false, true) => ConciseMessage::MainDiagnostic(main),
(true, false) => ConciseMessage::PrimaryAnnotation(annotation),
(false, false) => ConciseMessage::Both { main, annotation },
(true, true) => ConciseMessage::Empty,
if annotation.is_empty() {
ConciseMessage::MainDiagnostic(main)
} else {
ConciseMessage::Both { main, annotation }
}
}
}
@@ -888,6 +842,10 @@ impl Annotation {
pub fn hide_snippet(&mut self, yes: bool) {
self.hide_snippet = yes;
}
pub fn is_primary(&self) -> bool {
self.is_primary
}
}
/// Tags that can be associated with an annotation.
@@ -1508,28 +1466,10 @@ pub enum DiagnosticFormat {
pub enum ConciseMessage<'a> {
/// A diagnostic contains a non-empty main message and an empty
/// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "new" data model.
MainDiagnostic(&'a str),
/// A diagnostic contains an empty main message and a non-empty
/// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "old" data model.
PrimaryAnnotation(&'a str),
/// A diagnostic contains a non-empty main message and a non-empty
/// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "new" data model.
Both { main: &'a str, annotation: &'a str },
/// A diagnostic contains an empty main message and an empty
/// primary annotation message.
///
/// This indicates that the diagnostic is probably using the old
/// model.
Empty,
/// A custom concise message has been provided.
Custom(&'a str),
}
@@ -1540,13 +1480,9 @@ impl std::fmt::Display for ConciseMessage<'_> {
ConciseMessage::MainDiagnostic(main) => {
write!(f, "{main}")
}
ConciseMessage::PrimaryAnnotation(annotation) => {
write!(f, "{annotation}")
}
ConciseMessage::Both { main, annotation } => {
write!(f, "{main}: {annotation}")
}
ConciseMessage::Empty => Ok(()),
ConciseMessage::Custom(message) => {
write!(f, "{message}")
}

View File

@@ -21,7 +21,11 @@ use crate::source::source_text;
/// reflected in the changed AST offsets.
/// The other reason is that Ruff's AST doesn't implement `Eq` which Salsa requires
/// for determining if a query result is unchanged.
#[salsa::tracked(returns(ref), no_eq, heap_size=ruff_memory_usage::heap_size)]
///
/// The LRU capacity of 200 was picked without any empirical evidence that it's optimal,
/// instead it's a wild guess that it should be unlikely that incremental changes involve
/// more than 200 modules. Parsed ASTs within the same revision are never evicted by Salsa.
#[salsa::tracked(returns(ref), no_eq, heap_size=ruff_memory_usage::heap_size, lru=200)]
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
let _span = tracing::trace_span!("parsed_module", ?file).entered();
@@ -92,14 +96,9 @@ impl ParsedModule {
self.inner.store(None);
}
/// Returns the pointer address of this [`ParsedModule`].
///
/// The pointer uniquely identifies the module within the current Salsa revision,
/// regardless of whether particular [`ParsedModuleRef`] instances are garbage collected.
pub fn addr(&self) -> usize {
// Note that the outer `Arc` in `inner` is stable across garbage collection, while the inner
// `Arc` within the `ArcSwap` may change.
Arc::as_ptr(&self.inner).addr()
/// Returns the file to which this module belongs.
pub fn file(&self) -> File {
self.file
}
}

View File

@@ -667,6 +667,13 @@ impl Deref for SystemPathBuf {
}
}
impl AsRef<Path> for SystemPathBuf {
#[inline]
fn as_ref(&self) -> &Path {
self.0.as_std_path()
}
}
impl<P: AsRef<SystemPath>> FromIterator<P> for SystemPathBuf {
fn from_iter<I: IntoIterator<Item = P>>(iter: I) -> Self {
let mut buf = SystemPathBuf::new();

View File

@@ -42,13 +42,14 @@ impl<'a> Collector<'a> {
impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
fn visit_stmt(&mut self, stmt: &'ast Stmt) {
match stmt {
Stmt::ImportFrom(ast::StmtImportFrom {
names,
module,
level,
range: _,
node_index: _,
}) => {
Stmt::ImportFrom(import_from) => {
let ast::StmtImportFrom {
names,
module,
level,
range: _,
node_index: _,
} = &**import_from;
let module = module.as_deref();
let level = *level;
for alias in names {
@@ -87,24 +88,26 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
}
}
}
Stmt::Import(ast::StmtImport {
names,
range: _,
node_index: _,
}) => {
Stmt::Import(import_stmt) => {
let ast::StmtImport {
names,
range: _,
node_index: _,
} = &**import_stmt;
for alias in names {
if let Some(module_name) = ModuleName::new(alias.name.as_str()) {
self.imports.push(CollectedImport::Import(module_name));
}
}
}
Stmt::If(ast::StmtIf {
test,
body,
elif_else_clauses,
range: _,
node_index: _,
}) => {
Stmt::If(if_stmt) => {
let ast::StmtIf {
test,
body,
elif_else_clauses,
range: _,
node_index: _,
} = &**if_stmt;
// Skip TYPE_CHECKING blocks if not requested
if self.type_checking_imports || !is_type_checking_condition(test) {
self.visit_body(body);

View File

@@ -49,7 +49,7 @@ impl ModuleImports {
// Resolve the imports.
let mut resolved_imports = ModuleImports::default();
for import in imports {
for resolved in Resolver::new(db).resolve(import) {
for resolved in Resolver::new(db, path).resolve(import) {
if let Some(path) = resolved.as_system_path() {
resolved_imports.insert(path.to_path_buf());
}

View File

@@ -1,5 +1,9 @@
use ruff_db::files::FilePath;
use ty_python_semantic::{ModuleName, resolve_module, resolve_real_module};
use ruff_db::files::{File, FilePath, system_path_to_file};
use ruff_db::system::SystemPath;
use ty_python_semantic::{
ModuleName, resolve_module, resolve_module_confident, resolve_real_module,
resolve_real_module_confident,
};
use crate::ModuleDb;
use crate::collector::CollectedImport;
@@ -7,12 +11,15 @@ use crate::collector::CollectedImport;
/// Collect all imports for a given Python file.
pub(crate) struct Resolver<'a> {
db: &'a ModuleDb,
file: Option<File>,
}
impl<'a> Resolver<'a> {
/// Initialize a [`Resolver`] with a given [`ModuleDb`].
pub(crate) fn new(db: &'a ModuleDb) -> Self {
Self { db }
pub(crate) fn new(db: &'a ModuleDb, path: &SystemPath) -> Self {
// If we know the importing file we can potentially resolve more imports
let file = system_path_to_file(db, path).ok();
Self { db, file }
}
/// Resolve the [`CollectedImport`] into a [`FilePath`].
@@ -70,13 +77,21 @@ impl<'a> Resolver<'a> {
/// Resolves a module name to a module.
pub(crate) fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_module(self.db, module_name)?;
let module = if let Some(file) = self.file {
resolve_module(self.db, file, module_name)?
} else {
resolve_module_confident(self.db, module_name)?
};
Some(module.file(self.db)?.path(self.db))
}
/// Resolves a module name to a module (stubs not allowed).
fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_real_module(self.db, module_name)?;
let module = if let Some(file) = self.file {
resolve_real_module(self.db, file, module_name)?
} else {
resolve_real_module_confident(self.db, module_name)?
};
Some(module.file(self.db)?.path(self.db))
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.14.7"
version = "0.14.8"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -35,6 +35,7 @@ anyhow = { workspace = true }
bitflags = { workspace = true }
clap = { workspace = true, features = ["derive", "string"], optional = true }
colored = { workspace = true }
compact_str = { workspace = true }
fern = { workspace = true }
glob = { workspace = true }
globset = { workspace = true }

View File

@@ -28,9 +28,11 @@ yaml.load("{}", SafeLoader)
yaml.load("{}", yaml.SafeLoader)
yaml.load("{}", CSafeLoader)
yaml.load("{}", yaml.CSafeLoader)
yaml.load("{}", yaml.cyaml.CSafeLoader)
yaml.load("{}", NewSafeLoader)
yaml.load("{}", Loader=SafeLoader)
yaml.load("{}", Loader=yaml.SafeLoader)
yaml.load("{}", Loader=CSafeLoader)
yaml.load("{}", Loader=yaml.CSafeLoader)
yaml.load("{}", Loader=yaml.cyaml.CSafeLoader)
yaml.load("{}", Loader=NewSafeLoader)

View File

@@ -199,6 +199,9 @@ def bytes_okay(value=bytes(1)):
def int_okay(value=int("12")):
pass
# Allow immutable slice()
def slice_okay(value=slice(1,2)):
pass
# Allow immutable complex() value
def complex_okay(value=complex(1,2)):

View File

@@ -52,16 +52,16 @@ def not_broken5():
yield inner()
def not_broken6():
def broken3():
return (yield from [])
def not_broken7():
def broken4():
x = yield from []
return x
def not_broken8():
def broken5():
x = None
def inner(ex):
@@ -76,3 +76,13 @@ class NotBroken9(object):
def __await__(self):
yield from function()
return 42
async def broken6():
yield 1
return foo()
async def broken7():
yield 1
return [1, 2, 3]

View File

@@ -218,3 +218,26 @@ def should_not_fail(payload, Args):
Args:
The other arguments.
"""
# Test cases for Unpack[TypedDict] kwargs
from typing import TypedDict
from typing_extensions import Unpack
class User(TypedDict):
id: int
name: str
def function_with_unpack_args_should_not_fail(query: str, **kwargs: Unpack[User]):
"""Function with Unpack kwargs.
Args:
query: some arg
"""
def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]):
"""Function with Unpack kwargs but missing query arg documentation.
Args:
**kwargs: keyword arguments
"""

View File

@@ -17,3 +17,24 @@ def _():
# Valid yield scope
yield 3
# await is valid in any generator, sync or async
(await cor async for cor in f()) # ok
(await cor for cor in f()) # ok
# but not in comprehensions
[await cor async for cor in f()] # F704
{await cor async for cor in f()} # F704
{await cor: 1 async for cor in f()} # F704
[await cor for cor in f()] # F704
{await cor for cor in f()} # F704
{await cor: 1 for cor in f()} # F704
# or in the iterator of an async generator, which is evaluated in the parent
# scope
(cor async for cor in await f()) # F704
(await cor async for cor in [await c for c in f()]) # F704
# this is also okay because the comprehension is within the generator scope
([await c for c in cor] async for cor in f()) # ok

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

@@ -0,0 +1,88 @@
def f():
# These should both be ignored by the range suppression.
# ruff: disable[E741, F841]
I = 1
# ruff: enable[E741, F841]
def f():
# These should both be ignored by the implicit range suppression.
# Should also generate an "unmatched suppression" warning.
# ruff:disable[E741,F841]
I = 1
def f():
# Neither warning is ignored, and an "unmatched suppression"
# should be generated.
I = 1
# ruff: enable[E741, F841]
def f():
# One should be ignored by the range suppression, and
# the other logged to the user.
# ruff: disable[E741]
I = 1
# ruff: enable[E741]
def f():
# Test interleaved range suppressions. The first and last
# lines should each log a different warning, while the
# middle line should be completely silenced.
# ruff: disable[E741]
l = 0
# ruff: disable[F841]
O = 1
# ruff: enable[E741]
I = 2
# ruff: enable[F841]
def f():
# Neither of these are ignored and warnings are
# logged to user
# ruff: disable[E501]
I = 1
# ruff: enable[E501]
def f():
# These should both be ignored by the range suppression,
# and an unusued noqa diagnostic should be logged.
# 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

@@ -3,3 +3,5 @@ def func():
# Top-level await
await 1
([await c for c in cor] async for cor in func()) # ok

View File

@@ -0,0 +1,24 @@
async def gen():
yield 1
return 42
def gen(): # B901 but not a syntax error - not an async generator
yield 1
return 42
async def gen(): # ok - no value in return
yield 1
return
async def gen():
yield 1
return foo()
async def gen():
yield 1
return [1, 2, 3]
async def gen():
if True:
yield 1
return 10

View File

@@ -17,11 +17,12 @@ use ruff_python_ast::PythonVersion;
/// Run lint rules over a [`Stmt`] syntax node.
pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
match stmt {
Stmt::Global(ast::StmtGlobal {
names,
range: _,
node_index: _,
}) => {
Stmt::Global(global) => {
let ast::StmtGlobal {
names,
range: _,
node_index: _,
} = &**global;
if checker.is_rule_enabled(Rule::GlobalAtModuleLevel) {
pylint::rules::global_at_module_level(checker, stmt);
}
@@ -31,13 +32,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::Nonlocal(
nonlocal @ ast::StmtNonlocal {
Stmt::Nonlocal(nonlocal) => {
let ast::StmtNonlocal {
names,
range: _,
node_index: _,
},
) => {
} = &**nonlocal;
if checker.is_rule_enabled(Rule::AmbiguousVariableName) {
for name in names {
pycodestyle::rules::ambiguous_variable_name(checker, name, name.range());
@@ -47,8 +47,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::nonlocal_and_global(checker, nonlocal);
}
}
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
Stmt::FunctionDef(function_def) => {
let ast::StmtFunctionDef {
is_async,
name,
decorator_list,
@@ -58,8 +58,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
type_params: _,
range: _,
node_index: _,
},
) => {
} = &**function_def;
if checker.is_rule_enabled(Rule::DjangoNonLeadingReceiverDecorator) {
flake8_django::rules::non_leading_receiver_decorator(checker, decorator_list);
}
@@ -321,7 +320,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::in_function(checker, name, body);
}
if checker.is_rule_enabled(Rule::ReimplementedOperator) {
refurb::rules::reimplemented_operator(checker, &function_def.into());
refurb::rules::reimplemented_operator(checker, &(&**function_def).into());
}
if checker.is_rule_enabled(Rule::SslWithBadDefaults) {
flake8_bandit::rules::ssl_with_bad_defaults(checker, function_def);
@@ -356,8 +355,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::return_in_init(checker, stmt);
}
}
Stmt::ClassDef(
class_def @ ast::StmtClassDef {
Stmt::ClassDef(class_def) => {
let ast::StmtClassDef {
name,
arguments,
type_params: _,
@@ -365,8 +364,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
body,
range: _,
node_index: _,
},
) => {
} = &**class_def;
if checker.is_rule_enabled(Rule::NoClassmethodDecorator) {
pylint::rules::no_classmethod_decorator(checker, stmt);
}
@@ -526,11 +524,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
ruff::rules::implicit_class_var_in_dataclass(checker, class_def);
}
}
Stmt::Import(ast::StmtImport {
names,
range: _,
node_index: _,
}) => {
Stmt::Import(import) => {
let ast::StmtImport {
names,
range: _,
node_index: _,
} = &**import;
if checker.is_rule_enabled(Rule::MultipleImportsOnOneLine) {
pycodestyle::rules::multiple_imports_on_one_line(checker, stmt, names);
}
@@ -578,7 +577,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_tidy_imports::rules::banned_module_level_imports(checker, stmt);
}
for alias in names {
for alias in &import.names {
if checker.is_rule_enabled(Rule::NonAsciiImportName) {
pylint::rules::non_ascii_module_import(checker, alias);
}
@@ -604,7 +603,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.is_rule_enabled(Rule::ManualFromImport) {
pylint::rules::manual_from_import(checker, stmt, alias, names);
pylint::rules::manual_from_import(checker, stmt, alias, &import.names);
}
if checker.is_rule_enabled(Rule::ImportSelf) {
pylint::rules::import_self(checker, alias, checker.module.qualified_name());
@@ -681,17 +680,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::ImportFrom(
import_from @ ast::StmtImportFrom {
names,
module,
level,
range: _,
node_index: _,
},
) => {
let level = *level;
let module = module.as_deref();
Stmt::ImportFrom(import_from) => {
let level = import_from.level;
let module = import_from.module.as_deref();
if checker.is_rule_enabled(Rule::ModuleImportNotAtTopOfFile) {
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
}
@@ -699,7 +690,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::import_outside_top_level(checker, stmt);
}
if checker.is_rule_enabled(Rule::GlobalStatement) {
for name in names {
for name in &import_from.names {
if let Some(asname) = name.asname.as_ref() {
pylint::rules::global_statement(checker, asname);
} else {
@@ -708,7 +699,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.is_rule_enabled(Rule::NonAsciiImportName) {
for alias in names {
for alias in &import_from.names {
pylint::rules::non_ascii_module_import(checker, alias);
}
}
@@ -724,7 +715,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::UnnecessaryBuiltinImport) {
if let Some(module) = module {
pyupgrade::rules::unnecessary_builtin_import(
checker, stmt, module, names, level,
checker, stmt, module, &import_from.names, level,
);
}
}
@@ -760,7 +751,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
&stmt,
);
for alias in names {
for alias in &import_from.names {
if &alias.name == "*" {
continue;
}
@@ -789,7 +780,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_pyi::rules::from_future_import(checker, import_from);
}
}
for alias in names {
for alias in &import_from.names {
if module != Some("__future__") && &alias.name == "*" {
// F403
checker.report_diagnostic_if_enabled(
@@ -890,7 +881,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker,
level,
module,
names,
&import_from.names,
checker.module.qualified_name(),
);
}
@@ -906,14 +897,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_pyi::rules::bytestring_import(checker, import_from);
}
}
Stmt::Raise(raise @ ast::StmtRaise { exc, .. }) => {
Stmt::Raise(raise) => {
if checker.is_rule_enabled(Rule::RaiseNotImplemented) {
if let Some(expr) = exc {
if let Some(expr) = &raise.exc {
pyflakes::rules::raise_not_implemented(checker, expr);
}
}
if checker.is_rule_enabled(Rule::RaiseLiteral) {
if let Some(exc) = exc {
if let Some(exc) = &raise.exc {
flake8_bugbear::rules::raise_literal(checker, exc);
}
}
@@ -922,34 +913,34 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
Rule::FStringInException,
Rule::DotFormatInException,
]) {
if let Some(exc) = exc {
if let Some(exc) = &raise.exc {
flake8_errmsg::rules::string_in_exception(checker, stmt, exc);
}
}
if checker.is_rule_enabled(Rule::OSErrorAlias) {
if let Some(item) = exc {
if let Some(item) = &raise.exc {
pyupgrade::rules::os_error_alias_raise(checker, item);
}
}
if checker.is_rule_enabled(Rule::TimeoutErrorAlias) {
if checker.target_version() >= PythonVersion::PY310 {
if let Some(item) = exc {
if let Some(item) = &raise.exc {
pyupgrade::rules::timeout_error_alias_raise(checker, item);
}
}
}
if checker.is_rule_enabled(Rule::RaiseVanillaClass) {
if let Some(expr) = exc {
if let Some(expr) = &raise.exc {
tryceratops::rules::raise_vanilla_class(checker, expr);
}
}
if checker.is_rule_enabled(Rule::RaiseVanillaArgs) {
if let Some(expr) = exc {
if let Some(expr) = &raise.exc {
tryceratops::rules::raise_vanilla_args(checker, expr);
}
}
if checker.is_rule_enabled(Rule::UnnecessaryParenOnRaiseException) {
if let Some(expr) = exc {
if let Some(expr) = &raise.exc {
flake8_raise::rules::unnecessary_paren_on_raise_exception(checker, expr);
}
}
@@ -957,9 +948,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::misplaced_bare_raise(checker, raise);
}
}
Stmt::AugAssign(aug_assign @ ast::StmtAugAssign { target, .. }) => {
Stmt::AugAssign(aug_assign) => {
if checker.is_rule_enabled(Rule::GlobalStatement) {
if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
if let Expr::Name(ast::ExprName { id, .. }) = aug_assign.target.as_ref() {
pylint::rules::global_statement(checker, id);
}
}
@@ -967,13 +958,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
ruff::rules::sort_dunder_all_aug_assign(checker, aug_assign);
}
}
Stmt::If(
if_ @ ast::StmtIf {
test,
elif_else_clauses,
..
},
) => {
Stmt::If(if_) => {
if checker.is_rule_enabled(Rule::TooManyNestedBlocks) {
pylint::rules::too_many_nested_blocks(checker, stmt);
}
@@ -1036,33 +1021,33 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
Rule::PatchVersionComparison,
Rule::WrongTupleLengthVersionComparison,
]) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = if_.test.as_ref() {
for value in values {
flake8_pyi::rules::unrecognized_version_info(checker, value);
}
} else {
flake8_pyi::rules::unrecognized_version_info(checker, test);
flake8_pyi::rules::unrecognized_version_info(checker, &if_.test);
}
}
if checker.any_rule_enabled(&[
Rule::UnrecognizedPlatformCheck,
Rule::UnrecognizedPlatformName,
]) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = if_.test.as_ref() {
for value in values {
flake8_pyi::rules::unrecognized_platform(checker, value);
}
} else {
flake8_pyi::rules::unrecognized_platform(checker, test);
flake8_pyi::rules::unrecognized_platform(checker, &if_.test);
}
}
if checker.is_rule_enabled(Rule::ComplexIfStatementInStub) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = if_.test.as_ref() {
for value in values {
flake8_pyi::rules::complex_if_statement_in_stub(checker, value);
}
} else {
flake8_pyi::rules::complex_if_statement_in_stub(checker, test);
flake8_pyi::rules::complex_if_statement_in_stub(checker, &if_.test);
}
}
}
@@ -1091,10 +1076,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
let has_else_clause = elif_else_clauses.iter().any(|clause| clause.test.is_none());
let has_else_clause = if_.elif_else_clauses.iter().any(|clause| clause.test.is_none());
bad_version_info_comparison(checker, test.as_ref(), has_else_clause);
for clause in elif_else_clauses {
bad_version_info_comparison(checker, if_.test.as_ref(), has_else_clause);
for clause in &if_.elif_else_clauses {
if let Some(test) = clause.test.as_ref() {
bad_version_info_comparison(checker, test, has_else_clause);
}
@@ -1105,44 +1090,37 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
ruff::rules::if_key_in_dict_del(checker, if_);
}
if checker.is_rule_enabled(Rule::NeedlessElse) {
ruff::rules::needless_else(checker, if_.into());
ruff::rules::needless_else(checker, (&**if_).into());
}
}
Stmt::Assert(
assert_stmt @ ast::StmtAssert {
test,
msg,
range: _,
node_index: _,
},
) => {
Stmt::Assert(assert_stmt) => {
if !checker.semantic.in_type_checking_block() {
if checker.is_rule_enabled(Rule::Assert) {
flake8_bandit::rules::assert_used(checker, stmt);
}
}
if checker.is_rule_enabled(Rule::AssertTuple) {
pyflakes::rules::assert_tuple(checker, stmt, test);
pyflakes::rules::assert_tuple(checker, stmt, &assert_stmt.test);
}
if checker.is_rule_enabled(Rule::AssertFalse) {
flake8_bugbear::rules::assert_false(checker, stmt, test, msg.as_deref());
flake8_bugbear::rules::assert_false(checker, stmt, &assert_stmt.test, assert_stmt.msg.as_deref());
}
if checker.is_rule_enabled(Rule::PytestAssertAlwaysFalse) {
flake8_pytest_style::rules::assert_falsy(checker, stmt, test);
flake8_pytest_style::rules::assert_falsy(checker, stmt, &assert_stmt.test);
}
if checker.is_rule_enabled(Rule::PytestCompositeAssertion) {
flake8_pytest_style::rules::composite_condition(
checker,
stmt,
test,
msg.as_deref(),
&assert_stmt.test,
assert_stmt.msg.as_deref(),
);
}
if checker.is_rule_enabled(Rule::AssertOnStringLiteral) {
pylint::rules::assert_on_string_literal(checker, test);
pylint::rules::assert_on_string_literal(checker, &assert_stmt.test);
}
if checker.is_rule_enabled(Rule::InvalidMockAccess) {
pygrep_hooks::rules::non_existent_mock_method(checker, test);
pygrep_hooks::rules::non_existent_mock_method(checker, &assert_stmt.test);
}
if checker.is_rule_enabled(Rule::AssertWithPrintMessage) {
ruff::rules::assert_with_print_message(checker, assert_stmt);
@@ -1151,18 +1129,18 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
ruff::rules::invalid_assert_message_literal_argument(checker, assert_stmt);
}
}
Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => {
Stmt::With(with_stmt) => {
if checker.is_rule_enabled(Rule::TooManyNestedBlocks) {
pylint::rules::too_many_nested_blocks(checker, stmt);
}
if checker.is_rule_enabled(Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(checker, items);
flake8_bugbear::rules::assert_raises_exception(checker, &with_stmt.items);
}
if checker.is_rule_enabled(Rule::PytestRaisesWithMultipleStatements) {
flake8_pytest_style::rules::complex_raises(checker, stmt, items, body);
flake8_pytest_style::rules::complex_raises(checker, stmt, &with_stmt.items, &with_stmt.body);
}
if checker.is_rule_enabled(Rule::PytestWarnsWithMultipleStatements) {
flake8_pytest_style::rules::complex_warns(checker, stmt, items, body);
flake8_pytest_style::rules::complex_warns(checker, stmt, &with_stmt.items, &with_stmt.body);
}
if checker.is_rule_enabled(Rule::MultipleWithStatements) {
flake8_simplify::rules::multiple_with_statements(
@@ -1184,10 +1162,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::useless_with_lock(checker, with_stmt);
}
if checker.is_rule_enabled(Rule::CancelScopeNoCheckpoint) {
flake8_async::rules::cancel_scope_no_checkpoint(checker, with_stmt, items);
flake8_async::rules::cancel_scope_no_checkpoint(checker, with_stmt, &with_stmt.items);
}
}
Stmt::While(while_stmt @ ast::StmtWhile { body, orelse, .. }) => {
Stmt::While(while_stmt) => {
if checker.is_rule_enabled(Rule::TooManyNestedBlocks) {
pylint::rules::too_many_nested_blocks(checker, stmt);
}
@@ -1195,29 +1173,19 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Stmt(stmt));
}
if checker.is_rule_enabled(Rule::UselessElseOnLoop) {
pylint::rules::useless_else_on_loop(checker, stmt, body, orelse);
pylint::rules::useless_else_on_loop(checker, stmt, &while_stmt.body, &while_stmt.orelse);
}
if checker.is_rule_enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(checker, body);
perflint::rules::try_except_in_loop(checker, &while_stmt.body);
}
if checker.is_rule_enabled(Rule::AsyncBusyWait) {
flake8_async::rules::async_busy_wait(checker, while_stmt);
}
if checker.is_rule_enabled(Rule::NeedlessElse) {
ruff::rules::needless_else(checker, while_stmt.into());
ruff::rules::needless_else(checker, (&**while_stmt).into());
}
}
Stmt::For(
for_stmt @ ast::StmtFor {
target,
body,
iter,
orelse,
is_async,
range: _,
node_index: _,
},
) => {
Stmt::For(for_stmt) => {
if checker.is_rule_enabled(Rule::TooManyNestedBlocks) {
pylint::rules::too_many_nested_blocks(checker, stmt);
}
@@ -1235,25 +1203,25 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.analyze.for_loops.push(checker.semantic.snapshot());
}
if checker.is_rule_enabled(Rule::LoopVariableOverridesIterator) {
flake8_bugbear::rules::loop_variable_overrides_iterator(checker, target, iter);
flake8_bugbear::rules::loop_variable_overrides_iterator(checker, &for_stmt.target, &for_stmt.iter);
}
if checker.is_rule_enabled(Rule::FunctionUsesLoopVariable) {
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Stmt(stmt));
}
if checker.is_rule_enabled(Rule::ReuseOfGroupbyGenerator) {
flake8_bugbear::rules::reuse_of_groupby_generator(checker, target, body, iter);
flake8_bugbear::rules::reuse_of_groupby_generator(checker, &for_stmt.target, &for_stmt.body, &for_stmt.iter);
}
if checker.is_rule_enabled(Rule::UselessElseOnLoop) {
pylint::rules::useless_else_on_loop(checker, stmt, body, orelse);
pylint::rules::useless_else_on_loop(checker, stmt, &for_stmt.body, &for_stmt.orelse);
}
if checker.is_rule_enabled(Rule::RedefinedLoopName) {
pylint::rules::redefined_loop_name(checker, stmt);
}
if checker.is_rule_enabled(Rule::IterationOverSet) {
pylint::rules::iteration_over_set(checker, iter);
pylint::rules::iteration_over_set(checker, &for_stmt.iter);
}
if checker.is_rule_enabled(Rule::DictIterMissingItems) {
pylint::rules::dict_iter_missing_items(checker, target, iter);
pylint::rules::dict_iter_missing_items(checker, &for_stmt.target, &for_stmt.iter);
}
if checker.is_rule_enabled(Rule::ManualListCopy) {
perflint::rules::manual_list_copy(checker, for_stmt);
@@ -1263,7 +1231,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::modified_iterating_set(checker, for_stmt);
}
if checker.is_rule_enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(checker, iter, body);
perflint::rules::unnecessary_list_cast(checker, &for_stmt.iter, &for_stmt.body);
}
if checker.is_rule_enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup(checker, for_stmt);
@@ -1274,7 +1242,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::ReadlinesInFor) {
refurb::rules::readlines_in_for(checker, for_stmt);
}
if !*is_async {
if !for_stmt.is_async {
if checker.is_rule_enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
}
@@ -1282,7 +1250,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_simplify::rules::key_in_dict_for(checker, for_stmt);
}
if checker.is_rule_enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(checker, body);
perflint::rules::try_except_in_loop(checker, &for_stmt.body);
}
if checker.is_rule_enabled(Rule::ForLoopSetMutations) {
refurb::rules::for_loop_set_mutations(checker, for_stmt);
@@ -1292,141 +1260,133 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.is_rule_enabled(Rule::NeedlessElse) {
ruff::rules::needless_else(checker, for_stmt.into());
ruff::rules::needless_else(checker, (&**for_stmt).into());
}
}
Stmt::Try(
try_stmt @ ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
},
) => {
Stmt::Try(try_stmt) => {
if checker.is_rule_enabled(Rule::TooManyNestedBlocks) {
pylint::rules::too_many_nested_blocks(checker, stmt);
}
if checker.is_rule_enabled(Rule::JumpStatementInFinally) {
flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody);
flake8_bugbear::rules::jump_statement_in_finally(checker, &try_stmt.finalbody);
}
if checker.is_rule_enabled(Rule::ContinueInFinally) {
if checker.target_version() <= PythonVersion::PY38 {
pylint::rules::continue_in_finally(checker, finalbody);
pylint::rules::continue_in_finally(checker, &try_stmt.finalbody);
}
}
if checker.is_rule_enabled(Rule::DefaultExceptNotLast) {
pyflakes::rules::default_except_not_last(checker, handlers, checker.locator);
pyflakes::rules::default_except_not_last(checker, &try_stmt.handlers, checker.locator);
}
if checker.any_rule_enabled(&[
Rule::DuplicateHandlerException,
Rule::DuplicateTryBlockException,
]) {
flake8_bugbear::rules::duplicate_exceptions(checker, handlers);
flake8_bugbear::rules::duplicate_exceptions(checker, &try_stmt.handlers);
}
if checker.is_rule_enabled(Rule::RedundantTupleInExceptionHandler) {
flake8_bugbear::rules::redundant_tuple_in_exception_handler(checker, handlers);
flake8_bugbear::rules::redundant_tuple_in_exception_handler(checker, &try_stmt.handlers);
}
if checker.is_rule_enabled(Rule::OSErrorAlias) {
pyupgrade::rules::os_error_alias_handlers(checker, handlers);
pyupgrade::rules::os_error_alias_handlers(checker, &try_stmt.handlers);
}
if checker.is_rule_enabled(Rule::TimeoutErrorAlias) {
if checker.target_version() >= PythonVersion::PY310 {
pyupgrade::rules::timeout_error_alias_handlers(checker, handlers);
pyupgrade::rules::timeout_error_alias_handlers(checker, &try_stmt.handlers);
}
}
if checker.is_rule_enabled(Rule::PytestAssertInExcept) {
flake8_pytest_style::rules::assert_in_exception_handler(checker, handlers);
flake8_pytest_style::rules::assert_in_exception_handler(checker, &try_stmt.handlers);
}
if checker.is_rule_enabled(Rule::SuppressibleException) {
flake8_simplify::rules::suppressible_exception(
checker, stmt, body, handlers, orelse, finalbody,
checker, stmt, &try_stmt.body, &try_stmt.handlers, &try_stmt.orelse, &try_stmt.finalbody,
);
}
if checker.is_rule_enabled(Rule::ReturnInTryExceptFinally) {
flake8_simplify::rules::return_in_try_except_finally(
checker, body, handlers, finalbody,
checker, &try_stmt.body, &try_stmt.handlers, &try_stmt.finalbody,
);
}
if checker.is_rule_enabled(Rule::TryConsiderElse) {
tryceratops::rules::try_consider_else(checker, body, orelse, handlers);
tryceratops::rules::try_consider_else(checker, &try_stmt.body, &try_stmt.orelse, &try_stmt.handlers);
}
if checker.is_rule_enabled(Rule::VerboseRaise) {
tryceratops::rules::verbose_raise(checker, handlers);
tryceratops::rules::verbose_raise(checker, &try_stmt.handlers);
}
if checker.is_rule_enabled(Rule::VerboseLogMessage) {
tryceratops::rules::verbose_log_message(checker, handlers);
tryceratops::rules::verbose_log_message(checker, &try_stmt.handlers);
}
if checker.is_rule_enabled(Rule::RaiseWithinTry) {
tryceratops::rules::raise_within_try(checker, body, handlers);
tryceratops::rules::raise_within_try(checker, &try_stmt.body, &try_stmt.handlers);
}
if checker.is_rule_enabled(Rule::UselessTryExcept) {
tryceratops::rules::useless_try_except(checker, handlers);
tryceratops::rules::useless_try_except(checker, &try_stmt.handlers);
}
if checker.is_rule_enabled(Rule::ErrorInsteadOfException) {
tryceratops::rules::error_instead_of_exception(checker, handlers);
tryceratops::rules::error_instead_of_exception(checker, &try_stmt.handlers);
}
if checker.is_rule_enabled(Rule::NeedlessElse) {
ruff::rules::needless_else(checker, try_stmt.into());
ruff::rules::needless_else(checker, (&**try_stmt).into());
}
}
Stmt::Assign(assign @ ast::StmtAssign { targets, value, .. }) => {
Stmt::Assign(assign) => {
if checker.is_rule_enabled(Rule::SelfOrClsAssignment) {
for target in targets {
for target in &assign.targets {
pylint::rules::self_or_cls_assignment(checker, target);
}
}
if checker.is_rule_enabled(Rule::RedeclaredAssignedName) {
pylint::rules::redeclared_assigned_name(checker, targets);
pylint::rules::redeclared_assigned_name(checker, &assign.targets);
}
if checker.is_rule_enabled(Rule::LambdaAssignment) {
if let [target] = &targets[..] {
pycodestyle::rules::lambda_assignment(checker, target, value, None, stmt);
if let [target] = &assign.targets[..] {
pycodestyle::rules::lambda_assignment(checker, target, &assign.value, None, stmt);
}
}
if checker.is_rule_enabled(Rule::AssignmentToOsEnviron) {
flake8_bugbear::rules::assignment_to_os_environ(checker, targets);
flake8_bugbear::rules::assignment_to_os_environ(checker, &assign.targets);
}
if checker.is_rule_enabled(Rule::HardcodedPasswordString) {
flake8_bandit::rules::assign_hardcoded_password_string(checker, value, targets);
flake8_bandit::rules::assign_hardcoded_password_string(checker, &assign.value, &assign.targets);
}
if checker.is_rule_enabled(Rule::GlobalStatement) {
for target in targets {
if let Expr::Name(ast::ExprName { id, .. }) = target {
pylint::rules::global_statement(checker, id);
for target in &assign.targets {
if let Expr::Name(name_expr) = target {
pylint::rules::global_statement(checker, &name_expr.id);
}
}
}
if checker.is_rule_enabled(Rule::UselessMetaclassType) {
pyupgrade::rules::useless_metaclass_type(checker, stmt, value, targets);
pyupgrade::rules::useless_metaclass_type(checker, stmt, &assign.value, &assign.targets);
}
if checker.is_rule_enabled(Rule::ConvertTypedDictFunctionalToClass) {
pyupgrade::rules::convert_typed_dict_functional_to_class(
checker, stmt, targets, value,
checker, stmt, &assign.targets, &assign.value,
);
}
if checker.is_rule_enabled(Rule::ConvertNamedTupleFunctionalToClass) {
pyupgrade::rules::convert_named_tuple_functional_to_class(
checker, stmt, targets, value,
checker, stmt, &assign.targets, &assign.value,
);
}
if checker.is_rule_enabled(Rule::PandasDfVariableName) {
pandas_vet::rules::assignment_to_df(checker, targets);
pandas_vet::rules::assignment_to_df(checker, &assign.targets);
}
if checker.is_rule_enabled(Rule::AirflowVariableNameTaskIdMismatch) {
airflow::rules::variable_name_task_id(checker, targets, value);
airflow::rules::variable_name_task_id(checker, &assign.targets, &assign.value);
}
if checker.is_rule_enabled(Rule::SelfAssigningVariable) {
pylint::rules::self_assignment(checker, assign);
}
if checker.is_rule_enabled(Rule::TypeParamNameMismatch) {
pylint::rules::type_param_name_mismatch(checker, value, targets);
pylint::rules::type_param_name_mismatch(checker, &assign.value, &assign.targets);
}
if checker.is_rule_enabled(Rule::TypeNameIncorrectVariance) {
pylint::rules::type_name_incorrect_variance(checker, value);
pylint::rules::type_name_incorrect_variance(checker, &assign.value);
}
if checker.is_rule_enabled(Rule::TypeBivariance) {
pylint::rules::type_bivariance(checker, value);
pylint::rules::type_bivariance(checker, &assign.value);
}
if checker.is_rule_enabled(Rule::NonAugmentedAssignment) {
pylint::rules::non_augmented_assignment(checker, assign);
@@ -1449,14 +1409,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
.any(|scope| scope.kind.is_function())
{
if checker.is_rule_enabled(Rule::UnprefixedTypeParam) {
flake8_pyi::rules::prefix_type_params(checker, value, targets);
flake8_pyi::rules::prefix_type_params(checker, &assign.value, &assign.targets);
}
if checker.is_rule_enabled(Rule::AssignmentDefaultInStub) {
flake8_pyi::rules::assignment_default_in_stub(checker, targets, value);
flake8_pyi::rules::assignment_default_in_stub(checker, &assign.targets, &assign.value);
}
if checker.is_rule_enabled(Rule::UnannotatedAssignmentInStub) {
flake8_pyi::rules::unannotated_assignment_in_stub(
checker, targets, value,
checker, &assign.targets, &assign.value,
);
}
if checker.is_rule_enabled(Rule::ComplexAssignmentInStub) {
@@ -1464,7 +1424,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
if checker.is_rule_enabled(Rule::TypeAliasWithoutAnnotation) {
flake8_pyi::rules::type_alias_without_annotation(
checker, value, targets,
checker, &assign.value, &assign.targets,
);
}
}
@@ -1477,15 +1437,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pyupgrade::rules::non_pep695_type_alias_type(checker, assign);
}
}
Stmt::AnnAssign(
assign_stmt @ ast::StmtAnnAssign {
target,
value,
annotation,
..
},
) => {
if let Some(value) = value {
Stmt::AnnAssign(assign_stmt) => {
let target = &assign_stmt.target;
let annotation = &assign_stmt.annotation;
if let Some(value) = &assign_stmt.value {
if checker.is_rule_enabled(Rule::LambdaAssignment) {
pycodestyle::rules::lambda_assignment(
checker,
@@ -1506,7 +1461,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_bugbear::rules::unintentional_type_annotation(
checker,
target,
value.as_deref(),
assign_stmt.value.as_deref(),
stmt,
);
}
@@ -1514,7 +1469,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt);
}
if checker.is_rule_enabled(Rule::HardcodedPasswordString) {
if let Some(value) = value.as_deref() {
if let Some(value) = assign_stmt.value.as_deref() {
flake8_bandit::rules::assign_hardcoded_password_string(
checker,
value,
@@ -1526,7 +1481,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
ruff::rules::sort_dunder_all_ann_assign(checker, assign_stmt);
}
if checker.source_type.is_stub() {
if let Some(value) = value {
if let Some(value) = &assign_stmt.value {
if checker.is_rule_enabled(Rule::AssignmentDefaultInStub) {
// Ignore assignments in function bodies; those are covered by other rules.
if !checker
@@ -1563,7 +1518,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::TypeAlias(ast::StmtTypeAlias { name, .. }) => {
Stmt::TypeAlias(type_alias) => {
let name = &type_alias.name;
if checker.is_rule_enabled(Rule::SnakeCaseTypeAlias) {
flake8_pyi::rules::snake_case_type_alias(checker, name);
}
@@ -1571,17 +1527,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_pyi::rules::t_suffixed_type_alias(checker, name);
}
}
Stmt::Delete(
delete @ ast::StmtDelete {
targets,
range: _,
node_index: _,
},
) => {
Stmt::Delete(delete) => {
let targets = &delete.targets;
if checker.is_rule_enabled(Rule::GlobalStatement) {
for target in targets {
if let Expr::Name(ast::ExprName { id, .. }) = target {
pylint::rules::global_statement(checker, id);
if let Expr::Name(name_expr) = target {
pylint::rules::global_statement(checker, &name_expr.id);
}
}
}
@@ -1618,12 +1569,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::useless_exception_statement(checker, expr);
}
}
Stmt::Match(ast::StmtMatch {
subject: _,
cases,
range: _,
node_index: _,
}) => {
Stmt::Match(match_stmt) => {
let ast::StmtMatch {
subject: _,
cases,
range: _,
node_index: _,
} = &**match_stmt;
if checker.is_rule_enabled(Rule::NanComparison) {
pylint::rules::nan_comparison_match(checker, cases);
}

View File

@@ -35,6 +35,7 @@ use ruff_python_ast::helpers::{collect_import_from_member, is_docstring_stmt, to
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::str::Quote;
use ruff_python_ast::token::Tokens;
use ruff_python_ast::visitor::{Visitor, walk_except_handler, walk_pattern};
use ruff_python_ast::{
self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr,
@@ -48,7 +49,7 @@ use ruff_python_parser::semantic_errors::{
SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxErrorKind,
};
use ruff_python_parser::typing::{AnnotationKind, ParsedAnnotation, parse_type_annotation};
use ruff_python_parser::{ParseError, Parsed, Tokens};
use ruff_python_parser::{ParseError, Parsed};
use ruff_python_semantic::all::{DunderAllDefinition, DunderAllFlags};
use ruff_python_semantic::analyze::{imports, typing};
use ruff_python_semantic::{
@@ -68,6 +69,7 @@ use crate::noqa::NoqaMapping;
use crate::package::PackageRoot;
use crate::preview::is_undefined_export_in_dunder_init_enabled;
use crate::registry::Rule;
use crate::rules::flake8_bugbear::rules::ReturnInGenerator;
use crate::rules::pyflakes::rules::{
LateFutureImport, MultipleStarredExpressions, ReturnOutsideFunction,
UndefinedLocalWithNestedImportStarUsage, YieldOutsideFunction,
@@ -435,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> {
@@ -728,6 +739,12 @@ impl SemanticSyntaxContext for Checker<'_> {
self.report_diagnostic(NonlocalWithoutBinding { name }, error.range);
}
}
SemanticSyntaxErrorKind::ReturnInGenerator => {
// B901
if self.is_rule_enabled(Rule::ReturnInGenerator) {
self.report_diagnostic(ReturnInGenerator, error.range);
}
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
@@ -746,6 +763,7 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. }
| SemanticSyntaxErrorKind::NonlocalAndGlobal(_)
| SemanticSyntaxErrorKind::AnnotatedGlobal(_)
| SemanticSyntaxErrorKind::TypeParameterDefaultOrder(_)
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_) => {
self.semantic_errors.borrow_mut().push(error);
}
@@ -764,7 +782,10 @@ impl SemanticSyntaxContext for Checker<'_> {
for scope in self.semantic.current_scopes() {
match scope.kind {
ScopeKind::Class(_) | ScopeKind::Lambda(_) => return false,
ScopeKind::Function(ast::StmtFunctionDef { is_async, .. }) => return *is_async,
ScopeKind::Function(function_def) => {
let is_async = &function_def.is_async;
return *is_async;
}
ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
@@ -779,6 +800,10 @@ impl SemanticSyntaxContext for Checker<'_> {
match scope.kind {
ScopeKind::Class(_) => return false,
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
ScopeKind::Generator {
kind: GeneratorKind::Generator,
..
} => return true,
ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
@@ -828,14 +853,19 @@ impl SemanticSyntaxContext for Checker<'_> {
self.source_type.is_ipynb()
}
fn in_generator_scope(&self) -> bool {
matches!(
&self.semantic.current_scope().kind,
ScopeKind::Generator {
kind: GeneratorKind::Generator,
..
fn in_generator_context(&self) -> bool {
for scope in self.semantic.current_scopes() {
if matches!(
scope.kind,
ScopeKind::Generator {
kind: GeneratorKind::Generator,
..
}
) {
return true;
}
)
}
false
}
fn in_loop_context(&self) -> bool {
@@ -843,9 +873,13 @@ impl SemanticSyntaxContext for Checker<'_> {
for parent in self.semantic.current_statements().skip(1) {
match parent {
Stmt::For(ast::StmtFor { orelse, .. })
| Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
Stmt::For(node) => {
if !node.orelse.contains(child) {
return true;
}
}
Stmt::While(node) => {
if !node.orelse.contains(child) {
return true;
}
}
@@ -861,7 +895,8 @@ impl SemanticSyntaxContext for Checker<'_> {
fn is_bound_parameter(&self, name: &str) -> bool {
match self.semantic.current_scope().kind {
ScopeKind::Function(ast::StmtFunctionDef { parameters, .. }) => {
ScopeKind::Function(function_def) => {
let parameters = &function_def.parameters;
parameters.includes(name)
}
ScopeKind::Class(_)
@@ -905,12 +940,13 @@ impl<'a> Visitor<'a> for Checker<'a> {
{
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY;
}
Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) => {
Stmt::ImportFrom(node) => {
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY;
// Allow __future__ imports until we see a non-__future__ import.
if let Some("__future__") = module.as_deref() {
if names
if let Some("__future__") = node.module.as_deref() {
if node
.names
.iter()
.any(|alias| alias.name.as_str() == "annotations")
{
@@ -954,20 +990,22 @@ impl<'a> Visitor<'a> for Checker<'a> {
// Step 1: Binding
match stmt {
Stmt::AugAssign(ast::StmtAugAssign {
target,
op: _,
value: _,
range: _,
node_index: _,
}) => {
Stmt::AugAssign(node) => {
let ast::StmtAugAssign {
target,
op: _,
value: _,
range: _,
node_index: _,
} = &**node;
self.handle_node_load(target);
}
Stmt::Import(ast::StmtImport {
names,
range: _,
node_index: _,
}) => {
Stmt::Import(node) => {
let ast::StmtImport {
names,
range: _,
node_index: _,
} = &**node;
if self.semantic.at_top_level() {
self.importer.visit_import(stmt);
}
@@ -1016,13 +1054,14 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
}
}
Stmt::ImportFrom(ast::StmtImportFrom {
names,
module,
level,
range: _,
node_index: _,
}) => {
Stmt::ImportFrom(node) => {
let ast::StmtImportFrom {
names,
module,
level,
range: _,
node_index: _,
} = &**node;
if self.semantic.at_top_level() {
self.importer.visit_import(stmt);
}
@@ -1083,11 +1122,12 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
}
}
Stmt::Global(ast::StmtGlobal {
names,
range: _,
node_index: _,
}) => {
Stmt::Global(node) => {
let ast::StmtGlobal {
names,
range: _,
node_index: _,
} = &**node;
if !self.semantic.scope_id.is_global() {
for name in names {
let binding_id = self.semantic.global_scope().get(name);
@@ -1109,11 +1149,12 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
}
}
Stmt::Nonlocal(ast::StmtNonlocal {
names,
range: _,
node_index: _,
}) => {
Stmt::Nonlocal(node) => {
let ast::StmtNonlocal {
names,
range: _,
node_index: _,
} = &**node;
if !self.semantic.scope_id.is_global() {
for name in names {
if let Some((scope_id, binding_id)) = self.semantic.nonlocal(name) {
@@ -1147,17 +1188,13 @@ impl<'a> Visitor<'a> for Checker<'a> {
// Step 2: Traversal
match stmt {
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
name,
body,
parameters,
decorator_list,
returns,
type_params,
..
},
) => {
Stmt::FunctionDef(function_def) => {
let name = &function_def.name;
let body = &function_def.body;
let parameters = &function_def.parameters;
let decorator_list = &function_def.decorator_list;
let returns = &function_def.returns;
let type_params = &function_def.type_params;
// Visit the decorators and arguments, but avoid the body, which will be
// deferred.
for decorator in decorator_list {
@@ -1286,16 +1323,12 @@ impl<'a> Visitor<'a> for Checker<'a> {
BindingFlags::empty(),
);
}
Stmt::ClassDef(
class_def @ ast::StmtClassDef {
name,
body,
arguments,
decorator_list,
type_params,
..
},
) => {
Stmt::ClassDef(class_def) => {
let name = &class_def.name;
let body = &class_def.body;
let arguments = &class_def.arguments;
let decorator_list = &class_def.decorator_list;
let type_params = &class_def.type_params;
for decorator in decorator_list {
self.visit_decorator(decorator);
}
@@ -1342,30 +1375,20 @@ impl<'a> Visitor<'a> for Checker<'a> {
BindingFlags::empty(),
);
}
Stmt::TypeAlias(ast::StmtTypeAlias {
range: _,
node_index: _,
name,
type_params,
value,
}) => {
Stmt::TypeAlias(node) => {
self.semantic.push_scope(ScopeKind::Type);
if let Some(type_params) = type_params {
if let Some(type_params) = &node.type_params {
self.visit_type_params(type_params);
}
self.visit_deferred_type_alias_value(value);
self.visit_deferred_type_alias_value(&node.value);
self.semantic.pop_scope();
self.visit_expr(name);
self.visit_expr(&node.name);
}
Stmt::Try(
try_node @ ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
},
) => {
Stmt::Try(try_node) => {
let body = &try_node.body;
let handlers = &try_node.handlers;
let orelse = &try_node.orelse;
let finalbody = &try_node.finalbody;
// Iterate over the `body`, then the `handlers`, then the `orelse`, then the
// `finalbody`, but treat the body and the `orelse` as a single branch for
// flow analysis purposes.
@@ -1391,64 +1414,60 @@ impl<'a> Visitor<'a> for Checker<'a> {
self.visit_body(finalbody);
self.semantic.pop_branch();
}
Stmt::AnnAssign(ast::StmtAnnAssign {
target,
annotation,
value,
..
}) => {
Stmt::AnnAssign(node) => {
match AnnotationContext::from_model(
&self.semantic,
self.settings(),
self.target_version(),
) {
AnnotationContext::RuntimeRequired => {
self.visit_runtime_required_annotation(annotation);
self.visit_runtime_required_annotation(&node.annotation);
}
AnnotationContext::RuntimeEvaluated
if flake8_type_checking::helpers::is_dataclass_meta_annotation(
annotation,
&node.annotation,
self.semantic(),
) =>
{
self.visit_runtime_required_annotation(annotation);
self.visit_runtime_required_annotation(&node.annotation);
}
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(annotation);
self.visit_runtime_evaluated_annotation(&node.annotation);
}
AnnotationContext::TypingOnly
if flake8_type_checking::helpers::is_dataclass_meta_annotation(
annotation,
&node.annotation,
self.semantic(),
) =>
{
if let Expr::Subscript(subscript) = &**annotation {
if let Expr::Subscript(subscript) = &*node.annotation {
// Ex) `InitVar[str]`
self.visit_runtime_required_annotation(&subscript.value);
self.visit_annotation(&subscript.slice);
} else {
// Ex) `InitVar`
self.visit_runtime_required_annotation(annotation);
self.visit_runtime_required_annotation(&node.annotation);
}
}
AnnotationContext::TypingOnly => self.visit_annotation(annotation),
AnnotationContext::TypingOnly => self.visit_annotation(&node.annotation),
}
if let Some(expr) = value {
if self.semantic.match_typing_expr(annotation, "TypeAlias") {
if let Some(expr) = &node.value {
if self.semantic.match_typing_expr(&node.annotation, "TypeAlias") {
self.visit_annotated_type_alias_value(expr);
} else {
self.visit_expr(expr);
}
}
self.visit_expr(target);
self.visit_expr(&node.target);
}
Stmt::Assert(ast::StmtAssert {
test,
msg,
range: _,
node_index: _,
}) => {
Stmt::Assert(node) => {
let ast::StmtAssert {
test,
msg,
range: _,
node_index: _,
} = &**node;
let snapshot = self.semantic.flags;
self.semantic.flags |= SemanticModelFlags::ASSERT_STATEMENT;
self.visit_boolean_test(test);
@@ -1457,13 +1476,14 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
self.semantic.flags = snapshot;
}
Stmt::With(ast::StmtWith {
items,
body,
is_async: _,
range: _,
node_index: _,
}) => {
Stmt::With(node) => {
let ast::StmtWith {
items,
body,
is_async: _,
range: _,
node_index: _,
} = &**node;
for item in items {
self.visit_with_item(item);
}
@@ -1471,26 +1491,22 @@ impl<'a> Visitor<'a> for Checker<'a> {
self.visit_body(body);
self.semantic.pop_branch();
}
Stmt::While(ast::StmtWhile {
test,
body,
orelse,
range: _,
node_index: _,
}) => {
Stmt::While(node) => {
let ast::StmtWhile {
test,
body,
orelse,
range: _,
node_index: _,
} = &**node;
self.visit_boolean_test(test);
self.visit_body(body);
self.visit_body(orelse);
}
Stmt::If(
stmt_if @ ast::StmtIf {
test,
body,
elif_else_clauses,
range: _,
node_index: _,
},
) => {
Stmt::If(stmt_if) => {
let test = &stmt_if.test;
let body = &stmt_if.body;
let elif_else_clauses = &stmt_if.elif_else_clauses;
self.visit_boolean_test(test);
self.semantic.push_branch();
@@ -1515,14 +1531,14 @@ impl<'a> Visitor<'a> for Checker<'a> {
if self.semantic().at_top_level() || self.semantic().current_scope().kind.is_class() {
match stmt {
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
if let [Expr::Name(_)] = targets.as_slice() {
Stmt::Assign(node) => {
if let [Expr::Name(_)] = node.targets.as_slice() {
self.docstring_state =
DocstringState::Expected(ExpectedDocstringKind::Attribute);
}
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
if target.is_name_expr() {
Stmt::AnnAssign(node) => {
if node.target.is_name_expr() {
self.docstring_state =
DocstringState::Expected(ExpectedDocstringKind::Attribute);
}
@@ -2663,13 +2679,13 @@ impl<'a> Checker<'a> {
match parent {
Stmt::TypeAlias(_) => flags.insert(BindingFlags::DEFERRED_TYPE_ALIAS),
Stmt::AnnAssign(ast::StmtAnnAssign { annotation, .. }) => {
Stmt::AnnAssign(node) => {
// TODO: It is a bit unfortunate that we do this check twice
// maybe we should change how we visit this statement
// so the semantic flag for the type alias sticks around
// until after we've handled this store, so we can check
// the flag instead of duplicating this check
if self.semantic.match_typing_expr(annotation, "TypeAlias") {
if self.semantic.match_typing_expr(&node.annotation, "TypeAlias") {
flags.insert(BindingFlags::ANNOTATED_TYPE_ALIAS);
}
}
@@ -2680,22 +2696,22 @@ impl<'a> Checker<'a> {
if scope.kind.is_module()
&& match parent {
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
if let Some(Expr::Name(ast::ExprName { id, .. })) = targets.first() {
Stmt::Assign(node) => {
if let Some(Expr::Name(ast::ExprName { id, .. })) = node.targets.first() {
id == "__all__"
} else {
false
}
}
Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => {
if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
Stmt::AugAssign(node) => {
if let Expr::Name(ast::ExprName { id, .. }) = node.target.as_ref() {
id == "__all__"
} else {
false
}
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
Stmt::AnnAssign(node) => {
if let Expr::Name(ast::ExprName { id, .. }) = node.target.as_ref() {
id == "__all__"
} else {
false
@@ -2738,10 +2754,8 @@ impl<'a> Checker<'a> {
// Match the left-hand side of an annotated assignment without a value,
// like `x` in `x: int`. N.B. In stub files, these should be viewed
// as assignments on par with statements such as `x: int = 5`.
if matches!(
parent,
Stmt::AnnAssign(ast::StmtAnnAssign { value: None, .. })
) && !self.semantic.in_annotation()
if matches!(parent, Stmt::AnnAssign(node) if node.value.is_none())
&& !self.semantic.in_annotation()
{
self.add_binding(id, expr.range(), BindingKind::Annotation, flags);
return;
@@ -3013,19 +3027,16 @@ impl<'a> Checker<'a> {
let stmt = self.semantic.current_statement();
let Stmt::FunctionDef(ast::StmtFunctionDef {
body, parameters, ..
}) = stmt
else {
let Stmt::FunctionDef(node) = stmt else {
unreachable!("Expected Stmt::FunctionDef")
};
self.with_semantic_checker(|semantic, context| semantic.visit_stmt(stmt, context));
self.visit_parameters(parameters);
self.visit_parameters(&node.parameters);
// Set the docstring state before visiting the function body.
self.docstring_state = DocstringState::Expected(ExpectedDocstringKind::Function);
self.visit_body(body);
self.visit_body(&node.body);
}
}
self.semantic.restore(snapshot);

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::{TokenKind, Tokens};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange};

View File

@@ -12,17 +12,20 @@ use crate::fix::edits::delete_comment;
use crate::noqa::{
Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping,
};
use crate::preview::is_range_suppressions_enabled;
use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target;
use crate::rules::pygrep_hooks;
use crate::rules::ruff;
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA};
use crate::settings::LinterSettings;
use crate::suppression::Suppressions;
use crate::{Edit, Fix, Locator};
use super::ast::LintContext;
/// RUF100
#[expect(clippy::too_many_arguments)]
pub(crate) fn check_noqa(
context: &mut LintContext,
path: &Path,
@@ -31,6 +34,7 @@ pub(crate) fn check_noqa(
noqa_line_for: &NoqaMapping,
analyze_directives: bool,
settings: &LinterSettings,
suppressions: &Suppressions,
) -> Vec<usize> {
// Identify any codes that are globally exempted (within the current file).
let file_noqa_directives =
@@ -40,7 +44,7 @@ pub(crate) fn check_noqa(
let mut noqa_directives =
NoqaDirectives::from_commented_ranges(comment_ranges, &settings.external, path, locator);
if file_noqa_directives.is_empty() && noqa_directives.is_empty() {
if file_noqa_directives.is_empty() && noqa_directives.is_empty() && suppressions.is_empty() {
return Vec::new();
}
@@ -60,11 +64,19 @@ pub(crate) fn check_noqa(
continue;
}
// Apply file-level suppressions first
if exemption.contains_secondary_code(code) {
ignored_diagnostics.push(index);
continue;
}
// Apply ranged suppressions next
if is_range_suppressions_enabled(settings) && suppressions.check_diagnostic(diagnostic) {
ignored_diagnostics.push(index);
continue;
}
// Apply end-of-line noqa suppressions last
let noqa_offsets = diagnostic
.parent()
.into_iter()
@@ -107,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)
@@ -128,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));
}
@@ -224,6 +244,7 @@ pub(crate) fn check_noqa(
.map(|code| (*code).to_string())
.collect(),
}),
kind: ruff::rules::UnusedNOQAKind::Noqa,
},
directive.range(),
);

View File

@@ -4,9 +4,9 @@ use std::path::Path;
use ruff_notebook::CellOffsets;
use ruff_python_ast::PySourceType;
use ruff_python_ast::token::Tokens;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::Tokens;
use crate::Locator;
use crate::directives::TodoComment;

View File

@@ -5,8 +5,8 @@ use std::str::FromStr;
use bitflags::bitflags;
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_index::Indexer;
use ruff_python_parser::{TokenKind, Tokens};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};

View File

@@ -5,8 +5,8 @@ use std::iter::FusedIterator;
use std::slice::Iter;
use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt};
use ruff_python_ast::token::{Token, TokenKind, Tokens};
use ruff_python_ast::{self as ast, Stmt, Suite};
use ruff_python_parser::{Token, TokenKind, Tokens};
use ruff_source_file::UniversalNewlineIterator;
use ruff_text_size::{Ranged, TextSize};

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};
@@ -128,8 +127,8 @@ pub(crate) fn make_redundant_alias<'a>(
stmt: &Stmt,
) -> Vec<Edit> {
let aliases = match stmt {
Stmt::Import(ast::StmtImport { names, .. }) => names,
Stmt::ImportFrom(ast::StmtImportFrom { names, .. }) => names,
Stmt::Import(node) => &node.names,
Stmt::ImportFrom(node) => &node.names,
_ => {
return Vec::new();
}
@@ -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.
@@ -416,43 +404,46 @@ fn is_only<T: PartialEq>(vec: &[T], value: &T) -> bool {
/// Determine if a child is the only statement in its body.
fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
match parent {
Stmt::FunctionDef(ast::StmtFunctionDef { body, .. })
| Stmt::ClassDef(ast::StmtClassDef { body, .. })
| Stmt::With(ast::StmtWith { body, .. }) => {
if is_only(body, child) {
Stmt::FunctionDef(node) => {
if is_only(&node.body, child) {
return true;
}
}
Stmt::For(ast::StmtFor { body, orelse, .. })
| Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
if is_only(body, child) || is_only(orelse, child) {
Stmt::ClassDef(node) => {
if is_only(&node.body, child) {
return true;
}
}
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
if is_only(body, child)
|| elif_else_clauses
Stmt::With(node) => {
if is_only(&node.body, child) {
return true;
}
}
Stmt::For(node) => {
if is_only(&node.body, child) || is_only(&node.orelse, child) {
return true;
}
}
Stmt::While(node) => {
if is_only(&node.body, child) || is_only(&node.orelse, child) {
return true;
}
}
Stmt::If(node) => {
if is_only(&node.body, child)
|| node
.elif_else_clauses
.iter()
.any(|ast::ElifElseClause { body, .. }| is_only(body, child))
{
return true;
}
}
Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
}) => {
if is_only(body, child)
|| is_only(orelse, child)
|| is_only(finalbody, child)
|| handlers.iter().any(|handler| match handler {
Stmt::Try(node) => {
if is_only(&node.body, child)
|| is_only(&node.orelse, child)
|| is_only(&node.finalbody, child)
|| node.handlers.iter().any(|handler| match handler {
ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
body, ..
}) => is_only(body, child),
@@ -461,8 +452,8 @@ fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
return true;
}
}
Stmt::Match(ast::StmtMatch { cases, .. }) => {
if cases.iter().any(|case| is_only(&case.body, child)) {
Stmt::Match(node) => {
if node.cases.iter().any(|case| is_only(&case.body, child)) {
return true;
}
}

View File

@@ -9,10 +9,11 @@ use anyhow::Result;
use libcst_native as cst;
use ruff_diagnostics::Edit;
use ruff_python_ast::token::Tokens;
use ruff_python_ast::{self as ast, Expr, ModModule, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_importer::Insertion;
use ruff_python_parser::{Parsed, Tokens};
use ruff_python_parser::Parsed;
use ruff_python_semantic::{
ImportedName, MemberNameImport, ModuleNameImport, NameImport, SemanticModel,
};
@@ -235,9 +236,10 @@ impl<'a> Importer<'a> {
semantic: &SemanticModel<'a>,
type_checking_block: &Stmt,
) -> Option<&'a Stmt> {
let Stmt::If(ast::StmtIf { test, .. }) = type_checking_block else {
let Stmt::If(node) = type_checking_block else {
return None;
};
let test = &node.test;
let mut source = test;
while let Expr::Attribute(ast::ExprAttribute { value, .. }) = source.as_ref() {
@@ -452,17 +454,10 @@ impl<'a> Importer<'a> {
if stmt.start() >= at {
break;
}
if let Stmt::ImportFrom(ast::StmtImportFrom {
module: name,
names,
level,
range: _,
node_index: _,
}) = stmt
{
if *level == 0
&& name.as_ref().is_some_and(|name| name == module)
&& names.iter().all(|alias| alias.name.as_str() != "*")
if let Stmt::ImportFrom(node) = stmt {
if node.level == 0
&& node.module.as_ref().is_some_and(|name| name == module)
&& node.names.iter().all(|alias| alias.name.as_str() != "*")
{
import_from = Some(*stmt);
}

View File

@@ -46,6 +46,7 @@ pub mod rule_selector;
pub mod rules;
pub mod settings;
pub mod source_kind;
pub mod suppression;
mod text_helpers;
pub mod upstream_categories;
mod violation;

View File

@@ -32,6 +32,7 @@ use crate::rules::ruff::rules::test_rules::{self, TEST_RULES, TestRule};
use crate::settings::types::UnsafeFixes;
use crate::settings::{LinterSettings, TargetVersion, flags};
use crate::source_kind::SourceKind;
use crate::suppression::Suppressions;
use crate::{Locator, directives, fs};
pub(crate) mod float;
@@ -128,6 +129,7 @@ pub fn check_path(
source_type: PySourceType,
parsed: &Parsed<ModModule>,
target_version: TargetVersion,
suppressions: &Suppressions,
) -> Vec<Diagnostic> {
// Aggregate all diagnostics.
let mut context = LintContext::new(path, locator.contents(), settings);
@@ -339,6 +341,7 @@ pub fn check_path(
&directives.noqa_line_for,
parsed.has_valid_syntax(),
settings,
suppressions,
);
if noqa.is_enabled() {
for index in ignored.iter().rev() {
@@ -400,6 +403,9 @@ pub fn add_noqa_to_path(
&indexer,
);
// Parse range suppression comments
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
// Generate diagnostics, ignoring any existing `noqa` directives.
let diagnostics = check_path(
path,
@@ -414,6 +420,7 @@ pub fn add_noqa_to_path(
source_type,
&parsed,
target_version,
&suppressions,
);
// Add any missing `# noqa` pragmas.
@@ -427,6 +434,7 @@ pub fn add_noqa_to_path(
&directives.noqa_line_for,
stylist.line_ending(),
reason,
&suppressions,
)
}
@@ -461,6 +469,9 @@ pub fn lint_only(
&indexer,
);
// Parse range suppression comments
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
// Generate diagnostics.
let diagnostics = check_path(
path,
@@ -475,6 +486,7 @@ pub fn lint_only(
source_type,
&parsed,
target_version,
&suppressions,
);
LinterResult {
@@ -566,6 +578,9 @@ pub fn lint_fix<'a>(
&indexer,
);
// Parse range suppression comments
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
// Generate diagnostics.
let diagnostics = check_path(
path,
@@ -580,6 +595,7 @@ pub fn lint_fix<'a>(
source_type,
&parsed,
target_version,
&suppressions,
);
if iterations == 0 {
@@ -769,6 +785,7 @@ mod tests {
use crate::registry::Rule;
use crate::settings::LinterSettings;
use crate::source_kind::SourceKind;
use crate::suppression::Suppressions;
use crate::test::{TestedNotebook, assert_notebook_path, test_contents, test_snippet};
use crate::{Locator, assert_diagnostics, directives, settings};
@@ -944,6 +961,7 @@ mod tests {
&locator,
&indexer,
);
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
let mut diagnostics = check_path(
path,
None,
@@ -957,6 +975,7 @@ mod tests {
source_type,
&parsed,
target_version,
&suppressions,
);
diagnostics.sort_by(Diagnostic::ruff_start_ordering);
diagnostics
@@ -1043,6 +1062,7 @@ mod tests {
Rule::YieldFromInAsyncFunction,
Path::new("yield_from_in_async_function.py")
)]
#[test_case(Rule::ReturnInGenerator, Path::new("return_in_generator.py"))]
fn test_syntax_errors(rule: Rule, path: &Path) -> Result<()> {
let snapshot = path.to_string_lossy().to_string();
let path = Path::new("resources/test/fixtures/syntax_errors").join(path);

View File

@@ -20,12 +20,14 @@ use crate::Locator;
use crate::fs::relativize_path;
use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target;
use crate::suppression::Suppressions;
/// Generates an array of edits that matches the length of `messages`.
/// Each potential edit in the array is paired, in order, with the associated diagnostic.
/// Each edit will add a `noqa` comment to the appropriate line in the source to hide
/// the diagnostic. These edits may conflict with each other and should not be applied
/// simultaneously.
#[expect(clippy::too_many_arguments)]
pub fn generate_noqa_edits(
path: &Path,
diagnostics: &[Diagnostic],
@@ -34,11 +36,19 @@ pub fn generate_noqa_edits(
external: &[String],
noqa_line_for: &NoqaMapping,
line_ending: LineEnding,
suppressions: &Suppressions,
) -> Vec<Option<Edit>> {
let file_directives = FileNoqaDirectives::extract(locator, comment_ranges, external, path);
let exemption = FileExemption::from(&file_directives);
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
let comments = find_noqa_comments(
diagnostics,
locator,
&exemption,
&directives,
noqa_line_for,
suppressions,
);
build_noqa_edits_by_diagnostic(comments, locator, line_ending, None)
}
@@ -725,6 +735,7 @@ pub(crate) fn add_noqa(
noqa_line_for: &NoqaMapping,
line_ending: LineEnding,
reason: Option<&str>,
suppressions: &Suppressions,
) -> Result<usize> {
let (count, output) = add_noqa_inner(
path,
@@ -735,6 +746,7 @@ pub(crate) fn add_noqa(
noqa_line_for,
line_ending,
reason,
suppressions,
);
fs::write(path, output)?;
@@ -751,6 +763,7 @@ fn add_noqa_inner(
noqa_line_for: &NoqaMapping,
line_ending: LineEnding,
reason: Option<&str>,
suppressions: &Suppressions,
) -> (usize, String) {
let mut count = 0;
@@ -760,7 +773,14 @@ fn add_noqa_inner(
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
let comments = find_noqa_comments(
diagnostics,
locator,
&exemption,
&directives,
noqa_line_for,
suppressions,
);
let edits = build_noqa_edits_by_line(comments, locator, line_ending, reason);
@@ -859,6 +879,7 @@ fn find_noqa_comments<'a>(
exemption: &'a FileExemption,
directives: &'a NoqaDirectives,
noqa_line_for: &NoqaMapping,
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![];
@@ -875,6 +896,12 @@ fn find_noqa_comments<'a>(
continue;
}
// Apply ranged suppressions next
if suppressions.check_diagnostic(message) {
comments_by_line.push(None);
continue;
}
// Is the violation ignored by a `noqa` directive on the parent line?
if let Some(parent) = message.parent() {
if let Some(directive_line) =
@@ -1253,6 +1280,7 @@ mod tests {
use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon};
use crate::rules::pyflakes::rules::UnusedVariable;
use crate::rules::pyupgrade::rules::PrintfStringFormatting;
use crate::suppression::Suppressions;
use crate::{Edit, Violation};
use crate::{Locator, generate_noqa_edits};
@@ -2848,6 +2876,7 @@ mod tests {
&noqa_line_for,
LineEnding::Lf,
None,
&Suppressions::default(),
);
assert_eq!(count, 0);
assert_eq!(output, format!("{contents}"));
@@ -2872,6 +2901,7 @@ mod tests {
&noqa_line_for,
LineEnding::Lf,
None,
&Suppressions::default(),
);
assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: F841\n");
@@ -2903,6 +2933,7 @@ mod tests {
&noqa_line_for,
LineEnding::Lf,
None,
&Suppressions::default(),
);
assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: E741, F841\n");
@@ -2934,6 +2965,7 @@ mod tests {
&noqa_line_for,
LineEnding::Lf,
None,
&Suppressions::default(),
);
assert_eq!(count, 0);
assert_eq!(output, "x = 1 # noqa");
@@ -2956,6 +2988,7 @@ print(
let messages = [PrintfStringFormatting
.into_diagnostic(TextRange::new(12.into(), 79.into()), &source_file)];
let comment_ranges = CommentRanges::default();
let suppressions = Suppressions::default();
let edits = generate_noqa_edits(
path,
&messages,
@@ -2964,6 +2997,7 @@ print(
&[],
&noqa_line_for,
LineEnding::Lf,
&suppressions,
);
assert_eq!(
edits,
@@ -2987,6 +3021,7 @@ bar =
[UselessSemicolon.into_diagnostic(TextRange::new(4.into(), 5.into()), &source_file)];
let noqa_line_for = NoqaMapping::default();
let comment_ranges = CommentRanges::default();
let suppressions = Suppressions::default();
let edits = generate_noqa_edits(
path,
&messages,
@@ -2995,6 +3030,7 @@ bar =
&[],
&noqa_line_for,
LineEnding::Lf,
&suppressions,
);
assert_eq!(
edits,

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()
@@ -286,3 +291,8 @@ pub(crate) const fn is_s310_resolve_string_literal_bindings_enabled(
) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/21623
pub(crate) const fn is_range_suppressions_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -281,12 +281,10 @@ impl Renamer {
) -> Option<Edit> {
let statement = binding.statement(semantic)?;
let (ast::Stmt::Assign(ast::StmtAssign { value, .. })
| ast::Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value), ..
})) = statement
else {
return None;
let value = match statement {
ast::Stmt::Assign(node) => &node.value,
ast::Stmt::AnnAssign(node) => node.value.as_ref()?,
_ => return None,
};
let ast::ExprCall {

View File

@@ -448,11 +448,10 @@ fn is_kwarg_parameter(semantic: &SemanticModel, name: &ExprName) -> bool {
return false;
};
let binding = semantic.binding(binding_id);
let Some(Stmt::FunctionDef(StmtFunctionDef { parameters, .. })) = binding.statement(semantic)
else {
let Some(Stmt::FunctionDef(node)) = binding.statement(semantic) else {
return false;
};
parameters
node.parameters
.kwarg
.as_deref()
.is_some_and(|kwarg| kwarg.name.as_str() == name.id.as_str())

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

@@ -2,7 +2,7 @@
//!
//! See: <https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html>
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Stmt};
use ruff_python_ast::Stmt;
use ruff_text_size::Ranged;
use crate::Violation;
@@ -371,7 +371,8 @@ pub(crate) fn suspicious_imports(checker: &Checker, stmt: &Stmt) {
}
match stmt {
Stmt::Import(ast::StmtImport { names, .. }) => {
Stmt::Import(node) => {
let names = &node.names;
for name in names {
match name.name.as_str() {
"telnetlib" => {
@@ -421,8 +422,9 @@ pub(crate) fn suspicious_imports(checker: &Checker, stmt: &Stmt) {
}
}
}
Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) => {
let Some(identifier) = module else { return };
Stmt::ImportFrom(node) => {
let Some(identifier) = &node.module else { return };
let names = &node.names;
match identifier.as_str() {
"telnetlib" => {
checker.report_diagnostic_if_enabled(

View File

@@ -75,6 +75,7 @@ pub(crate) fn unsafe_yaml_load(checker: &Checker, call: &ast::ExprCall) {
qualified_name.segments(),
["yaml", "SafeLoader" | "CSafeLoader"]
| ["yaml", "loader", "SafeLoader" | "CSafeLoader"]
| ["yaml", "cyaml", "CSafeLoader"]
)
})
{

View File

@@ -154,10 +154,12 @@ impl<'a> StatementVisitor<'a> for ReraiseVisitor<'a> {
return;
}
match stmt {
Stmt::Raise(ast::StmtRaise { exc, cause, .. }) => {
Stmt::Raise(node) => {
let exc = node.exc.as_deref();
let cause = node.cause.as_deref();
// except Exception [as <name>]:
// raise [<exc> [from <cause>]]
let reraised = match (self.name, exc.as_deref(), cause.as_deref()) {
let reraised = match (self.name, exc, cause) {
// `raise`
(_, None, None) => true,
// `raise SomeExc from <name>`

View File

@@ -173,24 +173,21 @@ pub(crate) fn abstract_base_class(
// If an ABC declares an attribute by providing a type annotation
// but does not actually assign a value for that attribute,
// assume it is intended to be an "abstract attribute"
if matches!(
stmt,
Stmt::AnnAssign(ast::StmtAnnAssign { value: None, .. })
) {
has_abstract_method = true;
continue;
if let Stmt::AnnAssign(node) = stmt {
if node.value.is_none() {
has_abstract_method = true;
continue;
}
}
let Stmt::FunctionDef(ast::StmtFunctionDef {
decorator_list,
body,
name: method_name,
..
}) = stmt
else {
let Stmt::FunctionDef(node) = stmt else {
continue;
};
let decorator_list = &node.decorator_list;
let body = &node.body;
let method_name = &node.name;
let has_abstract_decorator = is_abstract(decorator_list, checker.semantic());
has_abstract_method |= has_abstract_decorator;

View File

@@ -51,7 +51,7 @@ impl AlwaysFixableViolation for AssertFalse {
}
fn assertion_error(msg: Option<&Expr>) -> Stmt {
Stmt::Raise(ast::StmtRaise {
Stmt::Raise(Box::new(ast::StmtRaise {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
exc: Some(Box::new(Expr::Call(ast::ExprCall {
@@ -75,7 +75,7 @@ fn assertion_error(msg: Option<&Expr>) -> Stmt {
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}))),
cause: None,
})
}))
}
/// B011

View File

@@ -114,14 +114,14 @@ pub(crate) fn class_as_data_structure(checker: &Checker, class_def: &ast::StmtCl
// assignment of a name to an attribute.
fn is_simple_assignment_to_attribute(stmt: &ast::Stmt) -> bool {
match stmt {
ast::Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
let [target] = targets.as_slice() else {
ast::Stmt::Assign(node) => {
let [target] = node.targets.as_slice() else {
return false;
};
target.is_attribute_expr() && value.is_name_expr()
target.is_attribute_expr() && node.value.is_name_expr()
}
ast::Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
target.is_attribute_expr() && value.as_ref().is_some_and(|val| val.is_name_expr())
ast::Stmt::AnnAssign(node) => {
node.target.is_attribute_expr() && node.value.as_ref().is_some_and(|val| val.is_name_expr())
}
_ => false,
}

View File

@@ -86,12 +86,10 @@ struct SuspiciousVariablesVisitor<'a> {
impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
parameters, body, ..
}) => {
Stmt::FunctionDef(node) => {
// Collect all loaded variable names.
let mut visitor = LoadedNamesVisitor::default();
visitor.visit_body(body);
visitor.visit_body(&node.body);
// Treat any non-arguments as "suspicious".
self.names
@@ -100,7 +98,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
return false;
}
if parameters.includes(&loaded.id) {
if node.parameters.includes(&loaded.id) {
return false;
}
@@ -242,18 +240,26 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
}
match stmt {
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
Stmt::Assign(node) => {
let mut visitor = NamesFromAssignmentsVisitor::default();
for expr in targets {
for expr in &node.targets {
visitor.visit_expr(expr);
}
self.names.extend(visitor.names);
}
Stmt::AugAssign(ast::StmtAugAssign { target, .. })
| Stmt::AnnAssign(ast::StmtAnnAssign { target, .. })
| Stmt::For(ast::StmtFor { target, .. }) => {
Stmt::AugAssign(node) => {
let mut visitor = NamesFromAssignmentsVisitor::default();
visitor.visit_expr(target);
visitor.visit_expr(&node.target);
self.names.extend(visitor.names);
}
Stmt::AnnAssign(node) => {
let mut visitor = NamesFromAssignmentsVisitor::default();
visitor.visit_expr(&node.target);
self.names.extend(visitor.names);
}
Stmt::For(node) => {
let mut visitor = NamesFromAssignmentsVisitor::default();
visitor.visit_expr(&node.target);
self.names.extend(visitor.names);
}
_ => {}

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Stmt};
use ruff_python_ast::Stmt;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_text_size::Ranged;
@@ -71,15 +71,23 @@ fn walk_stmt(checker: &Checker, body: &[Stmt], f: fn(&Stmt) -> bool) {
);
}
match stmt {
Stmt::While(ast::StmtWhile { body, .. }) | Stmt::For(ast::StmtFor { body, .. }) => {
walk_stmt(checker, body, Stmt::is_return_stmt);
Stmt::While(node) => {
walk_stmt(checker, &node.body, Stmt::is_return_stmt);
}
Stmt::If(ast::StmtIf { body, .. })
| Stmt::Try(ast::StmtTry { body, .. })
| Stmt::With(ast::StmtWith { body, .. }) => {
walk_stmt(checker, body, f);
Stmt::For(node) => {
walk_stmt(checker, &node.body, Stmt::is_return_stmt);
}
Stmt::Match(ast::StmtMatch { cases, .. }) => {
Stmt::If(node) => {
walk_stmt(checker, &node.body, f);
}
Stmt::Try(node) => {
walk_stmt(checker, &node.body, f);
}
Stmt::With(node) => {
walk_stmt(checker, &node.body, f);
}
Stmt::Match(node) => {
let cases = &node.cases;
for case in cases {
walk_stmt(checker, &case.body, f);
}

View File

@@ -5,8 +5,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::name::UnqualifiedName;
use ruff_python_ast::{
Expr, ExprAttribute, ExprCall, ExprSubscript, ExprTuple, Stmt, StmtAssign, StmtAugAssign,
StmtDelete, StmtFor, StmtIf,
self as ast, Expr, ExprAttribute, ExprCall, ExprSubscript, ExprTuple, Stmt, StmtFor,
visitor::{self, Visitor},
};
use ruff_text_size::TextRange;
@@ -242,43 +241,39 @@ impl<'a> Visitor<'a> for LoopMutationsVisitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match stmt {
// Ex) `del items[0]`
Stmt::Delete(StmtDelete {
range,
targets,
node_index: _,
}) => {
Stmt::Delete(node) => {
let ast::StmtDelete {
range,
targets,
node_index: _,
} = &**node;
self.handle_delete(*range, targets);
visitor::walk_stmt(self, stmt);
}
// Ex) `items[0] = 1`
Stmt::Assign(StmtAssign { range, targets, .. }) => {
self.handle_assign(*range, targets);
Stmt::Assign(node) => {
self.handle_assign(node.range, &node.targets);
visitor::walk_stmt(self, stmt);
}
// Ex) `items += [1]`
Stmt::AugAssign(StmtAugAssign { range, target, .. }) => {
self.handle_aug_assign(*range, target);
Stmt::AugAssign(node) => {
self.handle_aug_assign(node.range, &node.target);
visitor::walk_stmt(self, stmt);
}
// Ex) `if True: items.append(1)`
Stmt::If(StmtIf {
test,
body,
elif_else_clauses,
..
}) => {
Stmt::If(node) => {
// Handle the `if` branch.
self.branch += 1;
self.branches.push(self.branch);
self.visit_expr(test);
self.visit_body(body);
self.visit_expr(&node.test);
self.visit_body(&node.body);
self.branches.pop();
// Handle the `elif` and `else` branches.
for clause in elif_else_clauses {
for clause in &node.elif_else_clauses {
self.branch += 1;
self.branches.push(self.branch);
if let Some(test) = &clause.test {

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

@@ -1,6 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::statement_visitor;
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
use ruff_python_ast::{self as ast, Expr, Stmt, StmtFunctionDef};
use ruff_text_size::TextRange;
@@ -96,6 +95,11 @@ pub(crate) fn return_in_generator(checker: &Checker, function_def: &StmtFunction
return;
}
// Async functions are flagged by the `ReturnInGenerator` semantic syntax error.
if function_def.is_async {
return;
}
let mut visitor = ReturnInGeneratorVisitor::default();
visitor.visit_body(&function_def.body);
@@ -112,15 +116,9 @@ struct ReturnInGeneratorVisitor {
has_yield: bool,
}
impl StatementVisitor<'_> for ReturnInGeneratorVisitor {
impl Visitor<'_> for ReturnInGeneratorVisitor {
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Expr(ast::StmtExpr { value, .. }) => match **value {
Expr::Yield(_) | Expr::YieldFrom(_) => {
self.has_yield = true;
}
_ => {}
},
Stmt::FunctionDef(_) => {
// Do not recurse into nested functions; they're evaluated separately.
}
@@ -130,8 +128,19 @@ impl StatementVisitor<'_> for ReturnInGeneratorVisitor {
node_index: _,
}) => {
self.return_ = Some(*range);
walk_stmt(self, stmt);
}
_ => statement_visitor::walk_stmt(self, stmt),
_ => walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &Expr) {
match expr {
Expr::Lambda(_) => {}
Expr::Yield(_) | Expr::YieldFrom(_) => {
self.has_yield = true;
}
_ => walk_expr(self, expr),
}
}
}

View File

@@ -119,13 +119,11 @@ impl<'a> Visitor<'a> for GroupNameFinder<'a> {
return;
}
match stmt {
Stmt::For(ast::StmtFor {
target, iter, body, ..
}) => {
if self.name_matches(target) {
Stmt::For(node) => {
if self.name_matches(&node.target) {
self.overridden = true;
} else {
if self.name_matches(iter) {
if self.name_matches(&node.iter) {
self.increment_usage_count(1);
// This could happen when the group is being looped
// over multiple times:
@@ -136,36 +134,30 @@ impl<'a> Visitor<'a> for GroupNameFinder<'a> {
// for item in group:
// ...
if self.usage_count > 1 {
self.exprs.push(iter);
self.exprs.push(&node.iter);
}
}
self.nested = true;
visitor::walk_body(self, body);
visitor::walk_body(self, &node.body);
self.nested = false;
}
}
Stmt::While(ast::StmtWhile { body, .. }) => {
Stmt::While(node) => {
self.nested = true;
visitor::walk_body(self, body);
visitor::walk_body(self, &node.body);
self.nested = false;
}
Stmt::If(ast::StmtIf {
test,
body,
elif_else_clauses,
range: _,
node_index: _,
}) => {
Stmt::If(node) => {
// base if plus branches
let mut if_stack = Vec::with_capacity(1 + elif_else_clauses.len());
let mut if_stack = Vec::with_capacity(1 + node.elif_else_clauses.len());
// Initialize the vector with the count for the if branch.
if_stack.push(0);
self.counter_stack.push(if_stack);
self.visit_expr(test);
self.visit_body(body);
self.visit_expr(&node.test);
self.visit_body(&node.body);
for clause in elif_else_clauses {
for clause in &node.elif_else_clauses {
self.counter_stack.last_mut().unwrap().push(0);
self.visit_elif_else_clause(clause);
}
@@ -177,15 +169,10 @@ impl<'a> Visitor<'a> for GroupNameFinder<'a> {
self.increment_usage_count(max_count);
}
}
Stmt::Match(ast::StmtMatch {
subject,
cases,
range: _,
node_index: _,
}) => {
self.counter_stack.push(Vec::with_capacity(cases.len()));
self.visit_expr(subject);
for match_case in cases {
Stmt::Match(node) => {
self.counter_stack.push(Vec::with_capacity(node.cases.len()));
self.visit_expr(&node.subject);
for match_case in &node.cases {
self.counter_stack.last_mut().unwrap().push(0);
self.visit_match_case(match_case);
}
@@ -196,17 +183,17 @@ impl<'a> Visitor<'a> for GroupNameFinder<'a> {
self.increment_usage_count(max_count);
}
}
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
if targets.iter().any(|target| self.name_matches(target)) {
Stmt::Assign(node) => {
if node.targets.iter().any(|target| self.name_matches(target)) {
self.overridden = true;
} else {
self.visit_expr(value);
self.visit_expr(&node.value);
}
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
if self.name_matches(target) {
Stmt::AnnAssign(node) => {
if self.name_matches(&node.target) {
self.overridden = true;
} else if let Some(expr) = value {
} else if let Some(expr) = &node.value {
self.visit_expr(expr);
}
}

View File

@@ -66,7 +66,7 @@ impl AlwaysFixableViolation for SetAttrWithConstant {
}
fn assignment(obj: &Expr, name: &str, value: &Expr, generator: Generator) -> String {
let stmt = Stmt::Assign(ast::StmtAssign {
let stmt = Stmt::Assign(Box::new(ast::StmtAssign {
targets: vec![Expr::Attribute(ast::ExprAttribute {
value: Box::new(obj.clone()),
attr: Identifier::new(name.to_string(), TextRange::default()),
@@ -77,7 +77,7 @@ fn assignment(obj: &Expr, name: &str, value: &Expr, generator: Generator) -> Str
value: Box::new(value.clone()),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
}));
generator.stmt(&stmt)
}

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

@@ -236,227 +236,227 @@ help: Replace with `None`; initialize within function
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:239:20
--> B006_B008.py:242:20
|
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
240 | # B006 and B008
241 | # We should handle arbitrary nesting of these B008.
242 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
240 | pass
243 | pass
|
help: Replace with `None`; initialize within function
236 |
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 |
240 | # B006 and B008
241 | # We should handle arbitrary nesting of these B008.
- def nested_combo(a=[float(3), dt.datetime.now()]):
239 + def nested_combo(a=None):
240 | pass
241 |
242 |
242 + def nested_combo(a=None):
243 | pass
244 |
245 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:276:27
--> B006_B008.py:279:27
|
275 | def mutable_annotations(
276 | a: list[int] | None = [],
278 | def mutable_annotations(
279 | a: list[int] | None = [],
| ^^
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
help: Replace with `None`; initialize within function
273 |
274 |
275 | def mutable_annotations(
276 |
277 |
278 | def mutable_annotations(
- a: list[int] | None = [],
276 + a: list[int] | None = None,
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 + a: list[int] | None = None,
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:277:35
--> B006_B008.py:280:35
|
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | def mutable_annotations(
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
| ^^
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
help: Replace with `None`; initialize within function
274 |
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 |
278 | def mutable_annotations(
279 | a: list[int] | None = [],
- b: Optional[Dict[int, int]] = {},
277 + b: Optional[Dict[int, int]] = None,
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
280 + b: Optional[Dict[int, int]] = None,
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
283 | ):
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:278:62
--> B006_B008.py:281:62
|
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
283 | ):
|
help: Replace with `None`; initialize within function
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | def mutable_annotations(
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
278 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
281 | pass
281 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
283 | ):
284 | pass
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:279:80
--> B006_B008.py:282:80
|
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^
280 | ):
281 | pass
283 | ):
284 | pass
|
help: Replace with `None`; initialize within function
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
280 | ):
281 | pass
282 |
282 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
283 | ):
284 | pass
285 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:284:52
--> B006_B008.py:287:52
|
284 | def single_line_func_wrong(value: dict[str, str] = {}):
287 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
285 | """Docstring"""
288 | """Docstring"""
|
help: Replace with `None`; initialize within function
281 | pass
282 |
283 |
- def single_line_func_wrong(value: dict[str, str] = {}):
284 + def single_line_func_wrong(value: dict[str, str] = None):
285 | """Docstring"""
284 | pass
285 |
286 |
287 |
- def single_line_func_wrong(value: dict[str, str] = {}):
287 + def single_line_func_wrong(value: dict[str, str] = None):
288 | """Docstring"""
289 |
290 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:288:52
--> B006_B008.py:291:52
|
288 | def single_line_func_wrong(value: dict[str, str] = {}):
291 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
289 | """Docstring"""
290 | ...
292 | """Docstring"""
293 | ...
|
help: Replace with `None`; initialize within function
285 | """Docstring"""
286 |
287 |
288 | """Docstring"""
289 |
290 |
- def single_line_func_wrong(value: dict[str, str] = {}):
288 + def single_line_func_wrong(value: dict[str, str] = None):
289 | """Docstring"""
290 | ...
291 |
291 + def single_line_func_wrong(value: dict[str, str] = None):
292 | """Docstring"""
293 | ...
294 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:293:52
--> B006_B008.py:296:52
|
293 | def single_line_func_wrong(value: dict[str, str] = {}):
296 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
294 | """Docstring"""; ...
297 | """Docstring"""; ...
|
help: Replace with `None`; initialize within function
290 | ...
291 |
292 |
- def single_line_func_wrong(value: dict[str, str] = {}):
293 + def single_line_func_wrong(value: dict[str, str] = None):
294 | """Docstring"""; ...
293 | ...
294 |
295 |
296 |
- def single_line_func_wrong(value: dict[str, str] = {}):
296 + def single_line_func_wrong(value: dict[str, str] = None):
297 | """Docstring"""; ...
298 |
299 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:297:52
--> B006_B008.py:300:52
|
297 | def single_line_func_wrong(value: dict[str, str] = {}):
300 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
298 | """Docstring"""; \
299 | ...
301 | """Docstring"""; \
302 | ...
|
help: Replace with `None`; initialize within function
294 | """Docstring"""; ...
295 |
296 |
297 | """Docstring"""; ...
298 |
299 |
- def single_line_func_wrong(value: dict[str, str] = {}):
297 + def single_line_func_wrong(value: dict[str, str] = None):
298 | """Docstring"""; \
299 | ...
300 |
300 + def single_line_func_wrong(value: dict[str, str] = None):
301 | """Docstring"""; \
302 | ...
303 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:302:52
--> B006_B008.py:305:52
|
302 | def single_line_func_wrong(value: dict[str, str] = {
305 | def single_line_func_wrong(value: dict[str, str] = {
| ____________________________________________________^
303 | | # This is a comment
304 | | }):
306 | | # This is a comment
307 | | }):
| |_^
305 | """Docstring"""
308 | """Docstring"""
|
help: Replace with `None`; initialize within function
299 | ...
300 |
301 |
302 | ...
303 |
304 |
- def single_line_func_wrong(value: dict[str, str] = {
- # This is a comment
- }):
302 + def single_line_func_wrong(value: dict[str, str] = None):
303 | """Docstring"""
304 |
305 |
305 + def single_line_func_wrong(value: dict[str, str] = None):
306 | """Docstring"""
307 |
308 |
note: This is an unsafe fix and may change runtime behavior
B006 Do not use mutable data structures for argument defaults
--> B006_B008.py:308:52
--> B006_B008.py:311:52
|
308 | def single_line_func_wrong(value: dict[str, str] = {}) \
311 | def single_line_func_wrong(value: dict[str, str] = {}) \
| ^^
309 | : \
310 | """Docstring"""
312 | : \
313 | """Docstring"""
|
help: Replace with `None`; initialize within function
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:313:52
--> B006_B008.py:316:52
|
313 | def single_line_func_wrong(value: dict[str, str] = {}):
316 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
314 | """Docstring without newline"""
317 | """Docstring without newline"""
|
help: Replace with `None`; initialize within function
310 | """Docstring"""
311 |
312 |
313 | """Docstring"""
314 |
315 |
- def single_line_func_wrong(value: dict[str, str] = {}):
313 + def single_line_func_wrong(value: dict[str, str] = None):
314 | """Docstring without newline"""
316 + def single_line_func_wrong(value: dict[str, str] = None):
317 | """Docstring without newline"""
note: This is an unsafe fix and may change runtime behavior

View File

@@ -53,39 +53,39 @@ B008 Do not perform function call in argument defaults; instead, perform the cal
|
B008 Do not perform function call `dt.datetime.now` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
--> B006_B008.py:239:31
--> B006_B008.py:242:31
|
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
240 | # B006 and B008
241 | # We should handle arbitrary nesting of these B008.
242 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^
240 | pass
243 | pass
|
B008 Do not perform function call `map` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
--> B006_B008.py:245:22
--> B006_B008.py:248:22
|
243 | # Don't flag nested B006 since we can't guarantee that
244 | # it isn't made mutable by the outer operation.
245 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
246 | # Don't flag nested B006 since we can't guarantee that
247 | # it isn't made mutable by the outer operation.
248 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
246 | pass
249 | pass
|
B008 Do not perform function call `random.randint` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
--> B006_B008.py:250:19
--> B006_B008.py:253:19
|
249 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
252 | # B008-ception.
253 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
251 | pass
254 | pass
|
B008 Do not perform function call `dt.datetime.now` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
--> B006_B008.py:250:37
--> B006_B008.py:253:37
|
249 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
252 | # B008-ception.
253 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^
251 | pass
254 | pass
|

View File

@@ -21,3 +21,46 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf
37 |
38 | yield from not_broken()
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> B901.py:56:5
|
55 | def broken3():
56 | return (yield from [])
| ^^^^^^^^^^^^^^^^^^^^^^
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> B901.py:61:5
|
59 | def broken4():
60 | x = yield from []
61 | return x
| ^^^^^^^^
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> B901.py:72:5
|
71 | inner((yield from []))
72 | return x
| ^^^^^^^^
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> B901.py:83:5
|
81 | async def broken6():
82 | yield 1
83 | return foo()
| ^^^^^^^^^^^^
|
B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
--> B901.py:88:5
|
86 | async def broken7():
87 | yield 1
88 | return [1, 2, 3]
| ^^^^^^^^^^^^^^^^
|

View File

@@ -236,227 +236,227 @@ help: Replace with `None`; initialize within function
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:239:20
--> B006_B008.py:242:20
|
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
240 | # B006 and B008
241 | # We should handle arbitrary nesting of these B008.
242 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
240 | pass
243 | pass
|
help: Replace with `None`; initialize within function
236 |
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 |
240 | # B006 and B008
241 | # We should handle arbitrary nesting of these B008.
- def nested_combo(a=[float(3), dt.datetime.now()]):
239 + def nested_combo(a=None):
240 | pass
241 |
242 |
242 + def nested_combo(a=None):
243 | pass
244 |
245 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:276:27
--> B006_B008.py:279:27
|
275 | def mutable_annotations(
276 | a: list[int] | None = [],
278 | def mutable_annotations(
279 | a: list[int] | None = [],
| ^^
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
help: Replace with `None`; initialize within function
273 |
274 |
275 | def mutable_annotations(
276 |
277 |
278 | def mutable_annotations(
- a: list[int] | None = [],
276 + a: list[int] | None = None,
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 + a: list[int] | None = None,
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:277:35
--> B006_B008.py:280:35
|
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | def mutable_annotations(
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
| ^^
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
help: Replace with `None`; initialize within function
274 |
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 |
278 | def mutable_annotations(
279 | a: list[int] | None = [],
- b: Optional[Dict[int, int]] = {},
277 + b: Optional[Dict[int, int]] = None,
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
280 + b: Optional[Dict[int, int]] = None,
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
283 | ):
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:278:62
--> B006_B008.py:281:62
|
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
283 | ):
|
help: Replace with `None`; initialize within function
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | def mutable_annotations(
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
278 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
281 | pass
281 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
283 | ):
284 | pass
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:279:80
--> B006_B008.py:282:80
|
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^
280 | ):
281 | pass
283 | ):
284 | pass
|
help: Replace with `None`; initialize within function
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
280 | ):
281 | pass
282 |
282 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
283 | ):
284 | pass
285 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:284:52
--> B006_B008.py:287:52
|
284 | def single_line_func_wrong(value: dict[str, str] = {}):
287 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
285 | """Docstring"""
288 | """Docstring"""
|
help: Replace with `None`; initialize within function
281 | pass
282 |
283 |
- def single_line_func_wrong(value: dict[str, str] = {}):
284 + def single_line_func_wrong(value: dict[str, str] = None):
285 | """Docstring"""
284 | pass
285 |
286 |
287 |
- def single_line_func_wrong(value: dict[str, str] = {}):
287 + def single_line_func_wrong(value: dict[str, str] = None):
288 | """Docstring"""
289 |
290 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:288:52
--> B006_B008.py:291:52
|
288 | def single_line_func_wrong(value: dict[str, str] = {}):
291 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
289 | """Docstring"""
290 | ...
292 | """Docstring"""
293 | ...
|
help: Replace with `None`; initialize within function
285 | """Docstring"""
286 |
287 |
288 | """Docstring"""
289 |
290 |
- def single_line_func_wrong(value: dict[str, str] = {}):
288 + def single_line_func_wrong(value: dict[str, str] = None):
289 | """Docstring"""
290 | ...
291 |
291 + def single_line_func_wrong(value: dict[str, str] = None):
292 | """Docstring"""
293 | ...
294 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:293:52
--> B006_B008.py:296:52
|
293 | def single_line_func_wrong(value: dict[str, str] = {}):
296 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
294 | """Docstring"""; ...
297 | """Docstring"""; ...
|
help: Replace with `None`; initialize within function
290 | ...
291 |
292 |
- def single_line_func_wrong(value: dict[str, str] = {}):
293 + def single_line_func_wrong(value: dict[str, str] = None):
294 | """Docstring"""; ...
293 | ...
294 |
295 |
296 |
- def single_line_func_wrong(value: dict[str, str] = {}):
296 + def single_line_func_wrong(value: dict[str, str] = None):
297 | """Docstring"""; ...
298 |
299 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:297:52
--> B006_B008.py:300:52
|
297 | def single_line_func_wrong(value: dict[str, str] = {}):
300 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
298 | """Docstring"""; \
299 | ...
301 | """Docstring"""; \
302 | ...
|
help: Replace with `None`; initialize within function
294 | """Docstring"""; ...
295 |
296 |
297 | """Docstring"""; ...
298 |
299 |
- def single_line_func_wrong(value: dict[str, str] = {}):
297 + def single_line_func_wrong(value: dict[str, str] = None):
298 | """Docstring"""; \
299 | ...
300 |
300 + def single_line_func_wrong(value: dict[str, str] = None):
301 | """Docstring"""; \
302 | ...
303 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:302:52
--> B006_B008.py:305:52
|
302 | def single_line_func_wrong(value: dict[str, str] = {
305 | def single_line_func_wrong(value: dict[str, str] = {
| ____________________________________________________^
303 | | # This is a comment
304 | | }):
306 | | # This is a comment
307 | | }):
| |_^
305 | """Docstring"""
308 | """Docstring"""
|
help: Replace with `None`; initialize within function
299 | ...
300 |
301 |
302 | ...
303 |
304 |
- def single_line_func_wrong(value: dict[str, str] = {
- # This is a comment
- }):
302 + def single_line_func_wrong(value: dict[str, str] = None):
303 | """Docstring"""
304 |
305 |
305 + def single_line_func_wrong(value: dict[str, str] = None):
306 | """Docstring"""
307 |
308 |
note: This is an unsafe fix and may change runtime behavior
B006 Do not use mutable data structures for argument defaults
--> B006_B008.py:308:52
--> B006_B008.py:311:52
|
308 | def single_line_func_wrong(value: dict[str, str] = {}) \
311 | def single_line_func_wrong(value: dict[str, str] = {}) \
| ^^
309 | : \
310 | """Docstring"""
312 | : \
313 | """Docstring"""
|
help: Replace with `None`; initialize within function
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:313:52
--> B006_B008.py:316:52
|
313 | def single_line_func_wrong(value: dict[str, str] = {}):
316 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
314 | """Docstring without newline"""
317 | """Docstring without newline"""
|
help: Replace with `None`; initialize within function
310 | """Docstring"""
311 |
312 |
313 | """Docstring"""
314 |
315 |
- def single_line_func_wrong(value: dict[str, str] = {}):
313 + def single_line_func_wrong(value: dict[str, str] = None):
314 | """Docstring without newline"""
316 + def single_line_func_wrong(value: dict[str, str] = None):
317 | """Docstring without newline"""
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_index::Indexer;
use ruff_python_parser::{TokenKind, Tokens};
use ruff_text_size::{Ranged, TextRange};
use crate::Locator;

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_parser::TokenKind;
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_parser::TokenKind;
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_parser::TokenKind;
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

@@ -59,16 +59,20 @@ pub(crate) fn all_with_model_form(checker: &Checker, class_def: &ast::StmtClassD
}
for element in &class_def.body {
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
let Stmt::ClassDef(class_def_inner) = element else {
continue;
};
let name = &class_def_inner.name;
let body = &class_def_inner.body;
if name != "Meta" {
continue;
}
for element in body {
let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = element else {
let Stmt::Assign(assign) = element else {
continue;
};
let targets = &assign.targets;
let value = &assign.value;
for target in targets {
let Expr::Name(ast::ExprName { id, .. }) = target else {
continue;

View File

@@ -57,16 +57,19 @@ pub(crate) fn exclude_with_model_form(checker: &Checker, class_def: &ast::StmtCl
}
for element in &class_def.body {
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
let Stmt::ClassDef(class_def_inner) = element else {
continue;
};
let name = &class_def_inner.name;
let body = &class_def_inner.body;
if name != "Meta" {
continue;
}
for element in body {
let Stmt::Assign(ast::StmtAssign { targets, .. }) = element else {
let Stmt::Assign(assign) = element else {
continue;
};
let targets = &assign.targets;
for target in targets {
let Expr::Name(ast::ExprName { id, .. }) = target else {
continue;

View File

@@ -72,7 +72,7 @@ pub(crate) fn model_without_dunder_str(checker: &Checker, class_def: &ast::StmtC
fn has_dunder_method(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
analyze::class::any_super_class(class_def, semantic, &|class_def| {
class_def.body.iter().any(|val| match val {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => name == "__str__",
Stmt::FunctionDef(node) => node.name.as_str() == "__str__",
_ => false,
})
})
@@ -90,24 +90,25 @@ fn is_non_abstract_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel
/// Check if class is abstract, in terms of Django model inheritance.
fn is_model_abstract(class_def: &ast::StmtClassDef) -> bool {
for element in &class_def.body {
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
let Stmt::ClassDef(node) = element else {
continue;
};
if name != "Meta" {
if node.name.as_str() != "Meta" {
continue;
}
for element in body {
for element in &node.body {
match element {
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
if targets
Stmt::Assign(assign) => {
if assign
.targets
.iter()
.any(|target| is_abstract_true_assignment(target, Some(value)))
.any(|target| is_abstract_true_assignment(target, Some(&assign.value)))
{
return true;
}
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
if is_abstract_true_assignment(target, value.as_deref()) {
Stmt::AnnAssign(ann_assign) => {
if is_abstract_true_assignment(&ann_assign.target, ann_assign.value.as_deref()) {
return true;
}
}

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_ast::{Expr, Stmt};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_const_true;
@@ -62,10 +62,13 @@ pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) {
for statement in body {
let value = match statement {
Stmt::Assign(ast::StmtAssign { value, .. }) => value,
Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value), ..
}) => value,
Stmt::Assign(assign) => &assign.value,
Stmt::AnnAssign(ann_assign) => {
match &ann_assign.value {
Some(value) => value,
None => continue,
}
}
_ => continue,
};

View File

@@ -153,13 +153,13 @@ impl fmt::Display for ContentType {
fn get_element_type(element: &Stmt, semantic: &SemanticModel) -> Option<ContentType> {
match element {
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
Stmt::Assign(node) => {
if let Expr::Call(ast::ExprCall { func, .. }) = node.value.as_ref() {
if helpers::is_model_field(func, semantic) {
return Some(ContentType::FieldDeclaration);
}
}
let expr = targets.first()?;
let expr = node.targets.first()?;
let Expr::Name(ast::ExprName { id, .. }) = expr else {
return None;
};
@@ -169,14 +169,14 @@ fn get_element_type(element: &Stmt, semantic: &SemanticModel) -> Option<ContentT
None
}
}
Stmt::ClassDef(ast::StmtClassDef { name, .. }) => {
if name == "Meta" {
Stmt::ClassDef(node) => {
if node.name.as_str() == "Meta" {
Some(ContentType::MetaClass)
} else {
None
}
}
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => match name.as_str() {
Stmt::FunctionDef(node) => match node.name.as_str() {
name if is_dunder(name) => Some(ContentType::MagicMethod),
"save" => Some(ContentType::SaveMethod),
"get_absolute_url" => Some(ContentType::GetAbsoluteUrlMethod),

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

@@ -3,8 +3,8 @@ use std::borrow::Cow;
use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::StringFlags;
use ruff_python_ast::token::{Token, TokenKind, Tokens};
use ruff_python_index::Indexer;
use ruff_python_parser::{Token, TokenKind, Tokens};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextLen, TextRange};

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{Stmt, StmtTry};
use ruff_python_ast::Stmt;
use ruff_python_semantic::SemanticModel;
use ruff_text_size::{Ranged, TextSize};
@@ -8,9 +8,10 @@ pub(super) fn outside_handlers(offset: TextSize, semantic: &SemanticModel) -> bo
break;
}
let Stmt::Try(StmtTry { handlers, .. }) = stmt else {
let Stmt::Try(try_stmt) = stmt else {
continue;
};
let handlers = &try_stmt.handlers;
if handlers
.iter()

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 rustc_hash::FxHashSet;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_ast::{Expr, Stmt};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -59,15 +59,15 @@ pub(crate) fn duplicate_class_field_definition(checker: &Checker, body: &[Stmt])
for stmt in body {
// Extract the property name from the assignment statement.
let target = match stmt {
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
if let [Expr::Name(id)] = targets.as_slice() {
Stmt::Assign(assign_stmt) => {
if let [Expr::Name(id)] = assign_stmt.targets.as_slice() {
id
} else {
continue;
}
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
if let Expr::Name(id) = target.as_ref() {
Stmt::AnnAssign(ann_assign_stmt) => {
if let Expr::Name(id) = ann_assign_stmt.target.as_ref() {
id
} else {
continue;
@@ -78,20 +78,20 @@ pub(crate) fn duplicate_class_field_definition(checker: &Checker, body: &[Stmt])
// If this is an unrolled augmented assignment (e.g., `x = x + 1`), skip it.
match stmt {
Stmt::Assign(ast::StmtAssign { value, .. }) => {
if any_over_expr(value.as_ref(), &|expr| {
Stmt::Assign(assign_stmt) => {
if any_over_expr(assign_stmt.value.as_ref(), &|expr| {
expr.as_name_expr().is_some_and(|name| name.id == target.id)
}) {
continue;
}
}
Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value), ..
}) => {
if any_over_expr(value.as_ref(), &|expr| {
expr.as_name_expr().is_some_and(|name| name.id == target.id)
}) {
continue;
Stmt::AnnAssign(ann_assign_stmt) => {
if let Some(value) = &ann_assign_stmt.value {
if any_over_expr(value.as_ref(), &|expr| {
expr.as_name_expr().is_some_and(|name| name.id == target.id)
}) {
continue;
}
}
}
_ => continue,

View File

@@ -58,11 +58,11 @@ impl Violation for NonUniqueEnums {
pub(crate) fn non_unique_enums(checker: &Checker, parent: &Stmt, body: &[Stmt]) {
let semantic = checker.semantic();
let Stmt::ClassDef(parent) = parent else {
let Stmt::ClassDef(class_def) = parent else {
return;
};
if !parent.bases().iter().any(|expr| {
if !class_def.bases().iter().any(|expr| {
semantic
.resolve_qualified_name(expr)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["enum", "Enum"]))
@@ -72,9 +72,10 @@ pub(crate) fn non_unique_enums(checker: &Checker, parent: &Stmt, body: &[Stmt])
let mut seen_targets: FxHashSet<ComparableExpr> = FxHashSet::default();
for stmt in body {
let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else {
let Stmt::Assign(assign_stmt) = stmt else {
continue;
};
let value = &assign_stmt.value;
if is_call_to_enum_auto(semantic, value) {
continue;

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

@@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_parser::{TokenKind, Tokens};
use ruff_text_size::{Ranged, TextLen, TextSize};
use crate::checkers::ast::Checker;

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

@@ -1,4 +1,3 @@
use ruff_python_ast as ast;
use ruff_python_ast::Stmt;
use ruff_macros::{ViolationMetadata, derive_message_formats};
@@ -44,17 +43,15 @@ impl AlwaysFixableViolation for StrOrReprDefinedInStub {
/// PYI029
pub(crate) fn str_or_repr_defined_in_stub(checker: &Checker, stmt: &Stmt) {
let Stmt::FunctionDef(ast::StmtFunctionDef {
name,
decorator_list,
returns,
parameters,
..
}) = stmt
else {
let Stmt::FunctionDef(func_def) = stmt else {
return;
};
let name = &func_def.name;
let decorator_list = &func_def.decorator_list;
let returns = &func_def.returns;
let parameters = &func_def.parameters;
let Some(returns) = returns else {
return;
};

View File

@@ -196,15 +196,14 @@ pub(crate) fn unused_private_type_var(checker: &Checker, scope: &Scope) {
let Some(source) = binding.source else {
continue;
};
let stmt @ Stmt::Assign(ast::StmtAssign { targets, value, .. }) =
checker.semantic().statement(source)
else {
let stmt = checker.semantic().statement(source);
let Stmt::Assign(assign) = stmt else {
continue;
};
let [Expr::Name(ast::ExprName { id, .. })] = &targets[..] else {
let [Expr::Name(ast::ExprName { id, .. })] = &assign.targets[..] else {
continue;
};
let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else {
let Expr::Call(ast::ExprCall { func, .. }) = assign.value.as_ref() else {
continue;
};
@@ -317,18 +316,16 @@ pub(crate) fn unused_private_type_alias(checker: &Checker, scope: &Scope) {
fn extract_type_alias_name<'a>(stmt: &'a ast::Stmt, semantic: &SemanticModel) -> Option<&'a str> {
match stmt {
ast::Stmt::AnnAssign(ast::StmtAnnAssign {
target, annotation, ..
}) => {
let ast::ExprName { id, .. } = target.as_name_expr()?;
if semantic.match_typing_expr(annotation, "TypeAlias") {
ast::Stmt::AnnAssign(ann_assign) => {
let ast::ExprName { id, .. } = ann_assign.target.as_name_expr()?;
if semantic.match_typing_expr(&ann_assign.annotation, "TypeAlias") {
Some(id)
} else {
None
}
}
ast::Stmt::TypeAlias(ast::StmtTypeAlias { name, .. }) => {
let ast::ExprName { id, .. } = name.as_name_expr()?;
ast::Stmt::TypeAlias(type_alias) => {
let ast::ExprName { id, .. } = type_alias.name.as_name_expr()?;
Some(id)
}
_ => None,
@@ -388,9 +385,9 @@ fn extract_typeddict_name<'a>(stmt: &'a Stmt, semantic: &SemanticModel) -> Optio
// class Bar(typing.TypedDict, typing.Generic[T]):
// y: T
// ```
Stmt::ClassDef(class_def @ ast::StmtClassDef { name, .. }) => {
Stmt::ClassDef(class_def) => {
if class_def.bases().iter().any(is_typeddict) {
Some(name)
Some(&class_def.name)
} else {
None
}
@@ -402,12 +399,12 @@ fn extract_typeddict_name<'a>(stmt: &'a Stmt, semantic: &SemanticModel) -> Optio
// import typing
// Baz = typing.TypedDict("Baz", {"z": bytes})
// ```
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
let [target] = targets.as_slice() else {
Stmt::Assign(assign) => {
let [target] = assign.targets.as_slice() else {
return None;
};
let ast::ExprName { id, .. } = target.as_name_expr()?;
let ast::ExprCall { func, .. } = value.as_call_expr()?;
let ast::ExprCall { func, .. } = assign.value.as_call_expr()?;
if is_typeddict(func) { Some(id) } else { None }
}
_ => None,

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()),
)));
@@ -370,10 +369,10 @@ impl Violation for PytestUnittestRaisesAssertion {
/// PT027
pub(crate) fn unittest_raises_assertion_call(checker: &Checker, call: &ast::ExprCall) {
// Bindings in `with` statements are handled by `unittest_raises_assertion_bindings`.
if let Stmt::With(ast::StmtWith { items, .. }) = checker.semantic().current_statement() {
if let Stmt::With(with_stmt) = checker.semantic().current_statement() {
let call_ref = AnyNodeRef::from(call);
if items.iter().any(|item| {
if with_stmt.items.iter().any(|item| {
AnyNodeRef::from(&item.context_expr).ptr_eq(call_ref) && item.optional_vars.is_some()
}) {
return;
@@ -391,7 +390,11 @@ pub(crate) fn unittest_raises_assertion_binding(checker: &Checker, binding: &Bin
let semantic = checker.semantic();
let Some(Stmt::With(with)) = binding.statement(semantic) else {
let Some(stmt) = binding.statement(semantic) else {
return;
};
let Stmt::With(with) = stmt else {
return;
};

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

@@ -220,11 +220,11 @@ pub(crate) fn complex_raises(checker: &Checker, stmt: &Stmt, items: &[WithItem],
if raises_called {
let is_too_complex = if let [stmt] = body {
match stmt {
Stmt::With(ast::StmtWith { body, .. }) => is_non_trivial_with_body(body),
Stmt::With(with_stmt) => is_non_trivial_with_body(&with_stmt.body),
// Allow function and class definitions to test decorators.
Stmt::ClassDef(_) | Stmt::FunctionDef(_) => false,
// Allow empty `for` loops to test iterators.
Stmt::For(ast::StmtFor { body, .. }) => match &body[..] {
Stmt::For(for_stmt) => match &for_stmt.body[..] {
[Stmt::Pass(_)] => false,
[Stmt::Expr(ast::StmtExpr { value, .. })] => !value.is_ellipsis_literal_expr(),
_ => true,

View File

@@ -162,12 +162,12 @@ impl TryFrom<&str> for UnittestAssert {
}
fn assert(expr: &Expr, msg: Option<&Expr>) -> Stmt {
Stmt::Assert(ast::StmtAssert {
Stmt::Assert(Box::new(ast::StmtAssert {
test: Box::new(expr.clone()),
msg: msg.map(|msg| Box::new(msg.clone())),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
})
}))
}
fn compare(left: &Expr, cmp_op: CmpOp, right: &Expr) -> Expr {

View File

@@ -206,11 +206,11 @@ pub(crate) fn complex_warns(checker: &Checker, stmt: &Stmt, items: &[WithItem],
if warns_called {
let is_too_complex = if let [stmt] = body {
match stmt {
Stmt::With(ast::StmtWith { body, .. }) => is_non_trivial_with_body(body),
Stmt::With(with_stmt) => is_non_trivial_with_body(&with_stmt.body),
// Allow function and class definitions to test decorators.
Stmt::ClassDef(_) | Stmt::FunctionDef(_) => false,
// Allow empty `for` loops to test iterators.
Stmt::For(ast::StmtFor { body, .. }) => match &body[..] {
Stmt::For(for_stmt) => match &for_stmt.body[..] {
[Stmt::Pass(_)] => false,
[Stmt::Expr(ast::StmtExpr { value, .. })] => !value.is_ellipsis_literal_expr(),
_ => true,

View File

@@ -4,10 +4,10 @@ use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::{is_const_false, is_const_true};
use ruff_python_ast::stmt_if::elif_else_range;
use ruff_python_ast::token::TokenKind;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::whitespace::indentation;
use ruff_python_ast::{self as ast, Decorator, ElifElseClause, Expr, Stmt};
use ruff_python_parser::TokenKind;
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::visibility::is_property;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer, is_python_whitespace};
@@ -448,12 +448,12 @@ fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool {
return false;
};
let Stmt::FunctionDef(ast::StmtFunctionDef { returns, .. }) = semantic.statement(node_id)
let Stmt::FunctionDef(node) = semantic.statement(node_id)
else {
return false;
};
let Some(returns) = returns.as_ref() else {
let Some(returns) = node.returns.as_ref() else {
return false;
};
@@ -481,19 +481,16 @@ fn add_return_none(checker: &Checker, stmt: &Stmt, range: TextRange) {
fn has_implicit_return(checker: &Checker, stmt: &Stmt) -> bool {
match stmt {
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
if body
Stmt::If(node) => {
if node
.body
.last()
.is_some_and(|last| has_implicit_return(checker, last))
{
return true;
}
if elif_else_clauses.iter().any(|clause| {
if node.elif_else_clauses.iter().any(|clause| {
clause
.body
.last()
@@ -504,25 +501,33 @@ fn has_implicit_return(checker: &Checker, stmt: &Stmt) -> bool {
// Check if we don't have an else clause
matches!(
elif_else_clauses.last(),
node.elif_else_clauses.last(),
None | Some(ast::ElifElseClause { test: Some(_), .. })
)
}
Stmt::Assert(ast::StmtAssert { test, .. }) if is_const_false(test) => false,
Stmt::While(ast::StmtWhile { test, .. }) if is_const_true(test) => false,
Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => {
if let Some(last_stmt) = orelse.last() {
Stmt::Assert(node) if is_const_false(&node.test) => false,
Stmt::While(node) if is_const_true(&node.test) => false,
Stmt::For(node) => {
if let Some(last_stmt) = node.orelse.last() {
has_implicit_return(checker, last_stmt)
} else {
true
}
}
Stmt::Match(ast::StmtMatch { cases, .. }) => cases.iter().any(|case| {
Stmt::While(node) => {
if let Some(last_stmt) = node.orelse.last() {
has_implicit_return(checker, last_stmt)
} else {
true
}
}
Stmt::Match(node) => node.cases.iter().any(|case| {
case.body
.last()
.is_some_and(|last| has_implicit_return(checker, last))
}),
Stmt::With(ast::StmtWith { body, .. }) => body
Stmt::With(node) => node
.body
.last()
.is_some_and(|last_stmt| has_implicit_return(checker, last_stmt)),
Stmt::Return(_) | Stmt::Raise(_) | Stmt::Try(_) => false,

View File

@@ -62,11 +62,11 @@ impl<'semantic, 'data> ReturnVisitor<'semantic, 'data> {
impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match stmt {
Stmt::ClassDef(ast::StmtClassDef { decorator_list, .. }) => {
Stmt::ClassDef(node) => {
// Visit the decorators, etc.
self.sibling = Some(stmt);
self.parents.push(stmt);
for decorator in decorator_list {
for decorator in &node.decorator_list {
visitor::walk_decorator(self, decorator);
}
self.parents.pop();
@@ -74,12 +74,15 @@ impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> {
// But don't recurse into the body.
return;
}
Stmt::FunctionDef(ast::StmtFunctionDef {
parameters,
decorator_list,
returns,
..
}) => {
Stmt::FunctionDef(node) => {
let ast::StmtFunctionDef {
parameters,
decorator_list,
returns,
range: _,
node_index: _,
..
} = &**node;
// Visit the decorators, etc.
self.sibling = Some(stmt);
self.parents.push(stmt);
@@ -95,24 +98,30 @@ impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> {
// But don't recurse into the body.
return;
}
Stmt::Global(ast::StmtGlobal {
names,
range: _,
node_index: _,
})
| Stmt::Nonlocal(ast::StmtNonlocal {
names,
range: _,
node_index: _,
}) => {
Stmt::Global(node) => {
let ast::StmtGlobal {
names,
range: _,
node_index: _,
} = &**node;
self.stack
.non_locals
.extend(names.iter().map(Identifier::as_str));
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
Stmt::Nonlocal(node) => {
let ast::StmtNonlocal {
names,
range: _,
node_index: _,
} = &**node;
self.stack
.non_locals
.extend(names.iter().map(Identifier::as_str));
}
Stmt::AnnAssign(node) => {
// Ex) `x: int`
if value.is_none() {
if let Expr::Name(name) = target.as_ref() {
if node.value.is_none() {
if let Expr::Name(name) = node.target.as_ref() {
self.stack.annotations.insert(name.id.as_str());
}
}
@@ -140,11 +149,11 @@ impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> {
// x = f.read()
// return x
// ```
Stmt::With(with) => {
Stmt::With(with_node) => {
if let Some(stmt_assign) =
with.body.last().and_then(Stmt::as_assign_stmt)
with_node.body.last().and_then(Stmt::as_assign_stmt)
{
if !has_conditional_body(with, self.semantic) {
if !has_conditional_body(with_node, self.semantic) {
self.stack.assignment_return.push((
stmt_assign,
stmt_return,
@@ -159,11 +168,14 @@ impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> {
self.stack.returns.push(stmt_return);
}
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
Stmt::If(node) => {
let ast::StmtIf {
body,
elif_else_clauses,
range: _,
node_index: _,
..
} = &**node;
if let Some(first) = elif_else_clauses.first() {
self.stack.elifs_elses.push((body, first));
}

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

@@ -140,10 +140,10 @@ fn is_dunder_method(name: &str) -> bool {
}
fn is_exception_check(stmt: &Stmt) -> bool {
let Stmt::If(ast::StmtIf { body, .. }) = stmt else {
let Stmt::If(node) = stmt else {
return false;
};
matches!(body.as_slice(), [Stmt::Raise(_)])
matches!(node.body.as_slice(), [Stmt::Raise(_)])
}
/// SIM201

View File

@@ -68,18 +68,10 @@ impl Violation for MultipleWithStatements {
/// Returns a boolean indicating whether it's an async with statement, the items
/// and body.
fn next_with(body: &[Stmt]) -> Option<(bool, &[WithItem], &[Stmt])> {
let [
Stmt::With(ast::StmtWith {
is_async,
items,
body,
..
}),
] = body
else {
let [Stmt::With(node)] = body else {
return None;
};
Some((*is_async, items, body))
Some((node.is_async, &node.items, &node.body))
}
/// Check if `with_items` contains a single item which should not necessarily be
@@ -139,8 +131,8 @@ pub(crate) fn multiple_with_statements(
// with B(), C():
// print("hello")
// ```
if let Some(Stmt::With(ast::StmtWith { body, .. })) = with_parent {
if body.len() == 1 {
if let Some(Stmt::With(node)) = with_parent {
if node.body.len() == 1 {
return;
}
}

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