Compare commits

...

51 Commits

Author SHA1 Message Date
Alex Waygood
9ea95e9f24 rework check_call more 2025-06-26 18:58:25 +01:00
Alex Waygood
040bb0f932 consolidate how special-cased bindings for KnownClasses are determined 2025-06-26 17:33:45 +01:00
Alex Waygood
0150ac964d move special-cased bindings to class.rs 2025-06-26 17:33:45 +01:00
Micha Reiser
1dcdf7f41d [ty] Resolve python environment in Options::to_program_settings (#18960)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-06-26 17:57:16 +02:00
Victor Hugo Gomes
d00697621e [ruff] Fix false positives and negatives in RUF010 (#18690) 2025-06-26 17:53:52 +02:00
Andrew Gallant
76619b96e5 [ty] Fix rendering of long lines that are indented with tabs
It turns out that `annotate-snippets` doesn't do a great job of
consistently handling tabs. The intent of the implementation is clearly
to expand tabs into 4 ASCII whitespace characters. But there are a few
places where the column computation wasn't taking this expansion into
account. In particular, the `unicode-width` crate returns `None` for a
`\t` input, and `annotate-snippets` would in turn treat this as either
zero columns or one column. Both are wrong.

In patching this, it caused one of the existing `annotate-snippets`
tests to fail. I spent a fair bit of time on it trying to fix it before
coming to the conclusion that the test itself was wrong. In particular,
the annotation ranges are 4 bytes off. However, when the range was
wrong, the buggy code was rendering the example as intended since `\t`
characters were treated as taking up zero columns of space. Now that
they are correctly computed as taking up 4 columns of space, the offsets
of the test needed to be adjusted.

Fixes #670
2025-06-26 11:12:16 -04:00
Andrew Gallant
6e25cfba2b [ty] Add regression test for diagnostic rendering panic
This converts the MRE in #670 into a fixture test for
`annotate-snippets`.
2025-06-26 11:12:16 -04:00
Micha Reiser
76387295a5 [ty] Move venv and conda env discovery to SearchPath::from_settings (#18938)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-06-26 16:39:27 +02:00
David Peter
d04e63a6d9 [ty] Add regression-benchmark for attribute-assignment hang (#18957)
## Summary

Adds a new micro-benchmark as a regression test for
https://github.com/astral-sh/ty/issues/627.

## Test Plan

Ran the benchmark on the parent commit of
89d915a1e3,
and verified that it took > 1s, while it takes ~10 ms after the fix.
2025-06-26 15:21:08 +02:00
David Peter
86fd9b634e [ty] Format conflicting types as an enumeration (#18956)
## Summary

Format conflicting declared types as
```
`str`, `int` and `bytes`
```

Thanks to @AlexWaygood for the initial draft.

@dcreager, looking forward to your one-character follow-up PR.
2025-06-26 14:29:33 +02:00
David Peter
c0beb3412f [ty] Prevent union builder construction for just one declaration (#18954)
## Summary

Avoid the construction of the `DeclaredTypeBuilder` if there is just one
declared type.
2025-06-26 13:00:09 +02:00
David Peter
b01003f81d [ty] Infer nonlocal types as unions of all reachable bindings (#18750)
## Summary

This PR includes a behavioral change to how we infer types for public
uses of symbols within a module. Where we would previously use the type
that a use at the end of the scope would see, we now consider all
reachable bindings and union the results:

```py
x = None

def f():
    reveal_type(x)  # previously `Unknown | Literal[1]`, now `Unknown | None | Literal[1]`

f()

x = 1

f()
```

This helps especially in cases where the the end of the scope is not
reachable:

```py
def outer(x: int):
    def inner():
        reveal_type(x)  # previously `Unknown`, now `int`

    raise ValueError
```

This PR also proposes to skip the boundness analysis of public uses.
This is consistent with the "all reachable bindings" strategy, because
the implicit `x = <unbound>` binding is also always reachable, and we
would have to emit "possibly-unresolved" diagnostics for every public
use otherwise. Changing this behavior allows common use-cases like the
following to type check without any errors:

```py
def outer(flag: bool):
    if flag:
        x = 1

        def inner():
            print(x)  # previously: possibly-unresolved-reference, now: no error
```

closes https://github.com/astral-sh/ty/issues/210
closes https://github.com/astral-sh/ty/issues/607
closes https://github.com/astral-sh/ty/issues/699

## Follow up

It is now possible to resolve the following TODO, but I would like to do
that as a follow-up, because it requires some changes to how we treat
implicit attribute assignments, which could result in ecosystem changes
that I'd like to see separately.


315fb0f3da/crates/ty_python_semantic/src/semantic_index/builder.rs (L1095-L1117)

## Ecosystem analysis

[**Full report**](https://shark.fish/diff-public-types.html)

* This change obviously removes a lot of `possibly-unresolved-reference`
diagnostics (7818) because we do not analyze boundness for public uses
of symbols inside modules anymore.
* As the primary goal here, this change also removes a lot of
false-positive `unresolved-reference` diagnostics (231) in scenarios
like this:
    ```py
    def _(flag: bool):
        if flag:
            x = 1
    
            def inner():
                x
    
            raise
    ```
* This change also introduces some new false positives for cases like:
    ```py
    def _():
        x = None
    
        x = "test"
    
        def inner():
x.upper() # Attribute `upper` on type `Unknown | None | Literal["test"]`
is possibly unbound
    ```
We have test cases for these situations and it's plausible that we can
improve this in a follow-up.


## Test Plan

New Markdown tests
2025-06-26 12:24:40 +02:00
Victor Hugo Gomes
2362263d5e [pyflakes] Mark F504/F522/F523 autofix as unsafe if there's a call with side effect (#18839)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-26 08:48:29 +00:00
GiGaGon
170ccd80b4 [playground] Add ruff logo docs link to Header.tsx (#18947) 2025-06-26 08:54:45 +02:00
Alex Waygood
4a5715b97a [ty] Reduce the overwhelming complexity of TypeInferenceBuilder::infer_call_expression (#18943)
## Summary

This function is huge, and hugely indented. This PR breaks most of it
out into two helper functions: `KnownFunction::check_call()` and
`KnownClass::check_call`.

My immediate motivation is that we need to add yet more special cases to
this function in order to properly handle `tuple` instantiations and
instantiations of tuple subclasses. But I really don't relish the
thought of doing that with the function's current structure 😆

## Test Plan

Existing tests all pass. No new ones are added; this is a pure refactor
that should have no functional change.
2025-06-25 21:10:55 +01:00
Alex Waygood
c77e72ea1a [ty] Add subdiagnostic about empty bodies in more cases (#18942) 2025-06-25 20:25:00 +01:00
Micha Reiser
5d546c600a [ty] Move search path resolution to Options::to_program_settings (#18937) 2025-06-25 18:00:38 +02:00
Nikolas Hearp
8b22992988 [flake8-errmsg] Extend EM101 to support byte strings (#18867)
## Summary

Fixes #18765

## Test Plan

Added test
2025-06-25 10:53:56 -04:00
GiGaGon
f6def1c86d Move big rule implementations (#18931)
<!--
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? -->

Here's the part that was split out of #18906. I wanted to move these
into the rule files since the rest of the rules in
`deferred_scope`/`statement` have that same structure of implementations
being in the rule definition file. It also resolves the dilemma of where
to put the comment, at least for these rules.

## Test Plan

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

N/A, no test/functionality affected
2025-06-25 10:46:25 -04:00
Brent Westbrook
5aab49880a [pylint] Allow fix with comments and document performance implications (PLW3301) (#18936)
Summary
--

Closes #18849 by adding a `## Known issues` section describing the
potential performance issues when fixing nested iterables. I also
deleted the comment check since the fix is already unsafe and added a
note to the `## Fix safety` docs.

Test Plan
--

Existing tests, updated to allow a fix when comments are present since
the fix is already unsafe.
2025-06-25 09:29:23 -04:00
Brent Westbrook
7783cea14f [flake8-future-annotations] Add autofix (FA100) (#18903)
Summary
--

This PR resolves the easiest part of
https://github.com/astral-sh/ruff/issues/18502 by adding an autofix that
just adds
`from __future__ import annotations` at the top of the file, in the same
way
as FA102, which already has an identical unsafe fix.

Test Plan
--

Existing snapshots, updated to add the fixes.
2025-06-25 08:37:18 -04:00
Micha Reiser
c1fed55d51 Delete the ruff_python_resolver crate (#18933) 2025-06-25 12:53:13 +02:00
David Peter
689797a984 [ty] Type narrowing in comprehensions (#18934)
## Summary

Add type narrowing inside comprehensions:

```py
def _(xs: list[int | None]):
    [reveal_type(x) for x in xs if x is not None]  # revealed: int
```

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

## Test Plan

* New Markdown tests
* Made sure the example from https://github.com/astral-sh/ty/issues/680
now checks without errors
* Made sure that all removed ecosystem diagnostics were actually false
positives
2025-06-25 11:30:28 +02:00
Victor Hugo Gomes
66dbea90f1 [perflint] Fix false negative in PERF401 (#18866) 2025-06-25 10:44:32 +02:00
GiGaGon
d2684a00c6 Fix f-string interpolation escaping (#18882) 2025-06-25 10:04:15 +02:00
Robsdedude
2a0c5669f2 [refurb] Detect more exotic float literals in FURB164 (#18925) 2025-06-25 09:08:25 +02:00
GiGaGon
cb152b4725 [Internal] Use more report_diagnostic_if_enabled (#18924)
<!--
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? -->

From @ntBre
https://github.com/astral-sh/ruff/pull/18906#discussion_r2162843366 :
> This could be a good target for a follow-up PR, but we could fold
these `if checker.is_rule_enabled { checker.report_diagnostic` checks
into calls to `checker.report_diagnostic_if_enabled`. I didn't notice
these when adding that method.
> 
> Also, the docs on `Checker::report_diagnostic_if_enabled` and
`LintContext::report_diagnostic_if_enabled` are outdated now that the
`Rule` conversion is basically free 😅
> 
> No pressure to take on this refactor, just an idea if you're
interested!

This PR folds those calls. I also updated the doc comments by copying
from `report_diagnostic`.

Note: It seems odd to me that the doc comment for `Checker` says
`Diagnostic` while `LintContext` says `OldDiagnostic`, not sure if that
needs a bigger docs change to fix the inconsistency.

<details>
<summary>Python script to do the changes</summary>

This script assumes it is placed in the top level `ruff` directory (ie
next to `.git`/`crates`/`README.md`)

```py
import re
from copy import copy
from pathlib import Path

ruff_crates = Path(__file__).parent / "crates"

for path in ruff_crates.rglob("**/*.rs"):
    with path.open(encoding="utf-8", newline="") as f:
        original_content = f.read()
    if "is_rule_enabled" not in original_content or "report_diagnostic" not in original_content:
        continue
    original_content_position = 0
    changed_content = ""
    for match in re.finditer(r"(?m)(?:^[ \n]*|(?<=(?P<else>else )))if[ \n]+checker[ \n]*\.is_rule_enabled\([ \n]*Rule::\w+[ \n]*\)[ \n]*{[ \n]*checker\.report_diagnostic\(", original_content):
        # Content between last match and start of this one is unchanged
        changed_content += original_content[original_content_position:match.start()]
        # If this was an else if, a { needs to be added at the start
        if match.group("else"):
            changed_content += "{"
        # This will result in bad formatting, but the precommit cargo format will handle it
        changed_content += "checker.report_diagnostic_if_enabled("
        # Depth tracking would fail if a string/comment included a { or }, but unlikely given the context
        depth = 1
        position = match.end()
        while depth > 0:
            if original_content[position] == "{":
                depth += 1
            if original_content[position] == "}":
                depth -= 1
            position += 1
        # pos - 1 is the closing }
        changed_content += original_content[match.end():position - 1]
        # If this was an else if, a } needs to be added at the end
        if match.group("else"):
            changed_content += "}"
        # Skip the closing }
        original_content_position = position
        if original_content[original_content_position] == "\n":
            # If the } is followed by a \n, also skip it for better formatting
            original_content_position += 1
    # Add remaining content between last match and file end
    changed_content += original_content[original_content_position:]
    with path.open("w", encoding="utf-8", newline="") as f:
        f.write(changed_content)
```

</details>

## Test Plan

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

N/A, no tests/functionality affected.
2025-06-24 21:43:22 -04:00
GiGaGon
90f47e9b7b Add missing rule code comments (#18906)
<!--
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? -->

While making some of my other changes, I noticed some of the lints were
missing comments with their lint code/had the wrong numbered lint code.
These comments are super useful since they allow for very easily and
quickly finding the source code of a lint, so I decided to try and
normalize them.

Most of them were fairly straightforward, just adding a doc
comment/comment in the appropriate place.

I decided to make all of the `Pylint` rules have the `PL` prefix.
Previously it was split between no prefix and having prefix, but I
decided to normalize to with prefix since that's what's in the docs, and
the with prefix will show up on no prefix searches, while the reverse is
not true.

I also ran into a lot of rules with implementations in "non-standard"
places (where "standard" means inside a file matching the glob
`crates/ruff_linter/rules/*/rules/**/*.rs` and/or the same rule file
where the rule `struct`/`ViolationMetadata` is defined).

I decided to move all the implementations out of
`crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs` and
into their own files, since that is what the rest of the rules in
`deferred_scopes.rs` did, and those were just the outliers.

There were several rules which I did not end up moving, which you can
see as the extra paths I had to add to my python code besides the
"standard" glob. These rules are generally the error-type rules that
just wrap an error from the parser, and have very small
implementations/are very tightly linked to the module they are in, and
generally every rule of that type was implemented in module instead of
in the "standard" place.

Resolving that requires answering a question I don't think I'm equipped
to handle: Is the point of these comments to give quick access to the
rule definition/docs, or the rule implementation? For all the rules with
implementations in the "standard" location this isn't a problem, as they
are the same, but it is an issue for all of these error type rules. In
the end I chose to leave the implementations where they were, but I'm
not sure if that was the right choice.

<details>
<summary>Python script I wrote to find missing comments</summary>

This script assumes it is placed in the top level `ruff` directory (ie
next to `.git`/`crates`/`README.md`)

```py
import re
from copy import copy
from pathlib import Path

linter_to_code_prefix = {
    "Airflow": "AIR",
    "Eradicate": "ERA",
    "FastApi": "FAST",
    "Flake82020": "YTT",
    "Flake8Annotations": "ANN",
    "Flake8Async": "ASYNC",
    "Flake8Bandit": "S",
    "Flake8BlindExcept": "BLE",
    "Flake8BooleanTrap": "FBT",
    "Flake8Bugbear": "B",
    "Flake8Builtins": "A",
    "Flake8Commas": "COM",
    "Flake8Comprehensions": "C4",
    "Flake8Copyright": "CPY",
    "Flake8Datetimez": "DTZ",
    "Flake8Debugger": "T10",
    "Flake8Django": "DJ",
    "Flake8ErrMsg": "EM",
    "Flake8Executable": "EXE",
    "Flake8Fixme": "FIX",
    "Flake8FutureAnnotations": "FA",
    "Flake8GetText": "INT",
    "Flake8ImplicitStrConcat": "ISC",
    "Flake8ImportConventions": "ICN",
    "Flake8Logging": "LOG",
    "Flake8LoggingFormat": "G",
    "Flake8NoPep420": "INP",
    "Flake8Pie": "PIE",
    "Flake8Print": "T20",
    "Flake8Pyi": "PYI",
    "Flake8PytestStyle": "PT",
    "Flake8Quotes": "Q",
    "Flake8Raise": "RSE",
    "Flake8Return": "RET",
    "Flake8Self": "SLF",
    "Flake8Simplify": "SIM",
    "Flake8Slots": "SLOT",
    "Flake8TidyImports": "TID",
    "Flake8Todos": "TD",
    "Flake8TypeChecking": "TC",
    "Flake8UnusedArguments": "ARG",
    "Flake8UsePathlib": "PTH",
    "Flynt": "FLY",
    "Isort": "I",
    "McCabe": "C90",
    "Numpy": "NPY",
    "PandasVet": "PD",
    "PEP8Naming": "N",
    "Perflint": "PERF",
    "Pycodestyle": "",
    "Pydoclint": "DOC",
    "Pydocstyle": "D",
    "Pyflakes": "F",
    "PygrepHooks": "PGH",
    "Pylint": "PL",
    "Pyupgrade": "UP",
    "Refurb": "FURB",
    "Ruff": "RUF",
    "Tryceratops": "TRY",
}

ruff = Path(__file__).parent / "crates"

ruff_linter = ruff / "ruff_linter" / "src"

code_to_rule_name = {}

with open(ruff_linter / "codes.rs") as codes_file:
    for linter, code, rule_name in re.findall(
        # The (?<! skips ruff test rules
        # Only Preview|Stable rules are checked
        r"(?<!#\[cfg\(any\(feature = \"test-rules\", test\)\)\]\n)        \((\w+), \"(\w+)\"\) => \(RuleGroup::(?:Preview|Stable), [\w:]+::(\w+)\)",
        codes_file.read(),
    ):
        code_to_rule_name[linter_to_code_prefix[linter] + code] = (rule_name, [])

ruff_linter_rules = ruff_linter / "rules"
for rule_file_path in [
    *ruff_linter_rules.rglob("*/rules/**/*.rs"),
    ruff / "ruff_python_parser" / "src" / "semantic_errors.rs",
    ruff_linter / "pyproject_toml.rs",
    ruff_linter / "checkers" / "noqa.rs",
    ruff_linter / "checkers" / "ast" / "mod.rs",
    ruff_linter / "checkers" / "ast" / "analyze" / "unresolved_references.rs",
    ruff_linter / "checkers" / "ast" / "analyze" / "expression.rs",
    ruff_linter / "checkers" / "ast" / "analyze" / "statement.rs",
]:
    with open(rule_file_path, encoding="utf-8") as f:
        rule_file_content = f.read()
    for code, (rule, _) in copy(code_to_rule_name).items():
        if rule in rule_file_content:
            if f"// {code}" in rule_file_content or f", {code}" in rule_file_content:
                del code_to_rule_name[code]
            else:
                code_to_rule_name[code][1].append(rule_file_path)

for code, rule in code_to_rule_name.items():
    print(code, rule[0])
    for path in rule[1]:
        print(path)
```

</details>

## Test Plan

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

N/A, no tests/functionality affected.
2025-06-24 21:18:57 -04:00
Carl Meyer
62975b3ab2 [ty] eliminate is_fully_static (#18799)
## Summary

Having a recursive type method to check whether a type is fully static
is inefficient, unnecessary, and makes us overly strict about subtyping
relations.

It's inefficient because we end up re-walking the same types many times
to check for fully-static-ness.

It's unnecessary because we can check relations involving the dynamic
type appropriately, depending whether the relation is subtyping or
assignability.

We use the subtyping relation to simplify unions and intersections. We
can usefully consider that `S <: T` for gradual types also, as long as
it remains true that `S | T` is equivalent to `T` and `S & T` is
equivalent to `S`.

One conservative definition (implemented here) that satisfies this
requirement is that we consider `S <: T` if, for every possible pair of
materializations `S'` and `T'`, `S' <: T'`. Or put differently the top
materialization of `S` (`S+` -- the union of all possible
materializations of `S`) is a subtype of the bottom materialization of
`T` (`T-` -- the intersection of all possible materializations of `T`).
In the most basic cases we can usefully say that `Any <: object` and
that `Never <: Any`, and we can handle more complex cases inductively
from there.

This definition of subtyping for gradual subtypes is not reflexive
(`Any` is not a subtype of `Any`).

As a corollary, we also remove `is_gradual_equivalent_to` --
`is_equivalent_to` now has the meaning that `is_gradual_equivalent_to`
used to have. If necessary, we could restore an
`is_fully_static_equivalent_to` or similar (which would not do an
`is_fully_static` pre-check of the types, but would instead pass a
relation-kind enum down through a recursive equivalence check, similar
to `has_relation_to`), but so far this doesn't appear to be necessary.

Credit to @JelleZijlstra for the observation that `is_fully_static` is
unnecessary and overly restrictive on subtyping.

There is another possible definition of gradual subtyping: instead of
requiring that `S+ <: T-`, we could instead require that `S+ <: T+` and
`S- <: T-`. In other words, instead of requiring all materializations of
`S` to be a subtype of every materialization of `T`, we just require
that every materialization of `S` be a subtype of _some_ materialization
of `T`, and that every materialization of `T` be a supertype of some
materialization of `S`. This definition also preserves the core
invariant that `S <: T` implies that `S | T = T` and `S & T = S`, and it
restores reflexivity: under this definition, `Any` is a subtype of
`Any`, and for any equivalent types `S` and `T`, `S <: T` and `T <: S`.
But unfortunately, this definition breaks transitivity of subtyping,
because nominal subclasses in Python use assignability ("consistent
subtyping") to define acceptable overrides. This means that we may have
a class `A` with `def method(self) -> Any` and a subtype `B(A)` with
`def method(self) -> int`, since `int` is assignable to `Any`. This
means that if we have a protocol `P` with `def method(self) -> Any`, we
would have `B <: A` (from nominal subtyping) and `A <: P` (`Any` is a
subtype of `Any`), but not `B <: P` (`int` is not a subtype of `Any`).
Breaking transitivity of subtyping is not tenable, so we don't use this
definition of subtyping.

## Test Plan

Existing tests (modified in some cases to account for updated
semantics.)

Stable property tests pass at a million iterations:
`QUICKCHECK_TESTS=1000000 cargo test -p ty_python_semantic -- --ignored
types::property_tests::stable`

### Changes to property test type generation

Since we no longer have a method of categorizing built types as
fully-static or not-fully-static, I had to add a previously-discussed
feature to the property tests so that some tests can build types that
are known by construction to be fully static, because there are still
properties that only apply to fully-static types (for example,
reflexiveness of subtyping.)

## Changes to handling of `*args, **kwargs` signatures

This PR "discovered" that, once we allow non-fully-static types to
participate in subtyping under the above definitions, `(*args: Any,
**kwargs: Any) -> Any` is now a subtype of `() -> object`. This is true,
if we take a literal interpretation of the former signature: all
materializations of the parameters `*args: Any, **kwargs: Any` can
accept zero arguments, making the former signature a subtype of the
latter. But the spec actually says that `*args: Any, **kwargs: Any`
should be interpreted as equivalent to `...`, and that makes a
difference here: `(...) -> Any` is not a subtype of `() -> object`,
because (unlike a literal reading of `(*args: Any, **kwargs: Any)`),
`...` can materialize to _any_ signature, including a signature with
required positional arguments.

This matters for this PR because it makes the "any two types are both
assignable to their union" property test fail if we don't implement the
equivalence to `...`. Because `FunctionType.__call__` has the signature
`(*args: Any, **kwargs: Any) -> Any`, and if we take that at face value
it's a subtype of `() -> object`, making `FunctionType` a subtype of `()
-> object)` -- but then a function with a required argument is also a
subtype of `FunctionType`, but not a subtype of `() -> object`. So I
went ahead and implemented the equivalence to `...` in this PR.

## Ecosystem analysis

* Most of the ecosystem report are cases of improved union/intersection
simplification. For example, we can now simplify a union like `bool |
(bool & Unknown) | Unknown` to simply `bool | Unknown`, because we can
now observe that every possible materialization of `bool & Unknown` is
still a subtype of `bool` (whereas before we would set aside `bool &
Unknown` as a not-fully-static type.) This is clearly an improvement.
* The `possibly-unresolved-reference` errors in sockeye, pymongo,
ignite, scrapy and others are true positives for conditional imports
that were formerly silenced by bogus conflicting-declarations (which we
currently don't issue a diagnostic for), because we considered two
different declarations of `Unknown` to be conflicting (we used
`is_equivalent_to` not `is_gradual_equivalent_to`). In this PR that
distinction disappears and all equivalence is gradual, so a declaration
of `Unknown` no longer conflicts with a declaration of `Unknown`, which
then results in us surfacing the possibly-unbound error.
* We will now issue "redundant cast" for casting from a typevar with a
gradual bound to the same typevar (the hydra-zen diagnostic). This seems
like an improvement.
* The new diagnostics in bandersnatch are interesting. For some reason
primer in CI seems to be checking bandersnatch on Python 3.10 (not yet
sure why; this doesn't happen when I run it locally). But bandersnatch
uses `enum.StrEnum`, which doesn't exist on 3.10. That makes the `class
SimpleDigest(StrEnum)` a class that inherits from `Unknown` (and
bypasses our current TODO handling for accessing attributes on enum
classes, since we don't recognize it as an enum class at all). This PR
improves our understanding of assignability to classes that inherit from
`Any` / `Unknown`, and we now recognize that a string literal is not
assignable to a class inheriting `Any` or `Unknown`.
2025-06-24 18:02:05 -07:00
Dan Parizher
eee5a5a3d6 [docs] Typo fix for playground (#18929)
<!--
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? -->

I saw the smallest typo while familiarizing myself with the playground,
it bothered me so much I just had to make a PR for it 😂
2025-06-24 21:01:48 -04:00
Douglas Creager
66f50fb04b [ty] Add property test generators for variable-length tuples (#18901)
Add property test generators for the new variable-length tuples. This
covers homogeneous tuples as well.

The property tests did their job! This identified several fixes we
needed to make to various type property methods.

cf https://github.com/astral-sh/ruff/pull/18600#issuecomment-2993764471

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-06-24 18:13:47 -04:00
Robsdedude
919af9628d [pygrep_hooks] Add AsyncMock methods to invalid-mock-access (PGH005) (#18547)
## Summary
This PR expands PGH005 to also check for AsyncMock methods in the same
vein. E.g., currently `assert mock.not_called` is linted. This PR adds
the corresponding async assertions `assert mock.not_awaited()`.

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-06-24 17:27:21 -04:00
Alex Waygood
9d8cba4e8b [ty] Improve disjointness inference for NominalInstanceTypes and SubclassOfTypes (#18864)
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-06-24 20:27:37 +00:00
Josiah Kane
d89f75f9cc Fix link typo in ty's CONTRIBUTING.md (#18923) 2025-06-24 20:23:31 +00:00
Alex Waygood
e44c489273 [ty] Fix false positives when subscripting an object inferred as having an Intersection type (#18920) 2025-06-24 18:39:02 +00:00
chiri
3220242dec [flake8-use-pathlib] Add autofix for PTH202 (#18763)
<!--
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
/closes #2331
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
update snapshots
<!-- How was it tested? -->

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-06-24 17:58:31 +00:00
Andrew Gallant
6abafcb565 [ty] Add relative import completion tests
This tests things like `from ...foo import <CURSOR>`.

I had previously tested this on an ad hoc basis inside
of my editor, so the token state machine already recognizes
this pattern.

Ref https://github.com/astral-sh/ruff/pull/18830#discussion_r2159670033
2025-06-24 11:41:16 -04:00
Andrew Gallant
cef1a522dc [ty] Clarify what "cursor" means
This commit does a small refactor to combine the file and
cursor offset into a single type. I think this makes it
clearer that even if there are multiple files in the cursor
test, this one in particular corresponds to the file that
contains the `<CURSOR>` marker.
2025-06-24 11:41:16 -04:00
Andrew Gallant
40731f0589 [ty] Add a cursor test builder
This doesn't change any functionality of the cursor tests, but does
re-arrange the code a bit. Firstly, it's now in a builder. And secondly,
there's an API to add multiple files to the test (but exactly one must
have a `<CURSOR>` marker).
2025-06-24 11:41:16 -04:00
Andrew Gallant
1461137407 [ty] Enforce sort order of completions (#18917)
We achieve this by setting the "sort text" field of every completion.
Since we are trying to be smart about the order, we want the client to
respect our order.

Prior to this change, VS Code was re-sorting completions in
lexicographic order. This in turn resulted in dunder attributes
appearing before "normal" attributes.
2025-06-24 11:31:08 -04:00
K
47653ca88a [formatter] Fix missing blank lines before decorated classes in .pyi files (#18888)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-24 16:25:44 +02:00
Brent Westbrook
02ae8e1210 Apply fix availability and applicability when adding to DiagnosticGuard and remove NoqaCode::rule (#18834)
## Summary

This PR removes the last two places we were using `NoqaCode::rule` in
`linter.rs` (see
https://github.com/astral-sh/ruff/pull/18391#discussion_r2154637329 and
https://github.com/astral-sh/ruff/pull/18391#discussion_r2154649726) by
checking whether fixes are actually desired before adding them to a
`DiagnosticGuard`. I implemented this by storing a `Violation`'s `Rule`
on the `DiagnosticGuard` so that we could check if it was enabled in the
embedded `LinterSettings` when trying to set a fix.

All of the corresponding `set_fix` methods on `OldDiagnostic` were now
unused (except in tests where I just set `.fix` directly), so I moved
these to the guard instead of keeping both sets.

The very last place where we were using `NoqaCode::rule` was in the
cache. I just reverted this to parsing the `Rule` from the name. I had
forgotten to update the comment there anyway. Hopefully this doesn't
cause too much of a perf hit.

In terms of binary size, we're back down almost to where `main` was two
days ago
(https://github.com/astral-sh/ruff/pull/18391#discussion_r2155034320):

```
41,559,344 bytes for main 2 days ago
41,669,840 bytes for #18391
41,653,760 bytes for main now (after #18391 merged)
41,602,224 bytes for this branch
```

Only 43 kb up, but that shouldn't all be me this time :)

## Test Plan

Existing tests and benchmarks on this PR
2025-06-24 10:08:36 -04:00
David Peter
0edbd6c390 py-fuzzer: allow relative executable paths (#18915)
## Summary

I tried running `py-fuzzer` using executables in the current working
directory, but that failed with:
```
▶ uvx --from ./python/py-fuzzer --reinstall fuzz --test-executable ./ty_feature --bin=ty --baseline-executable ./ty_main --only-new-bugs 0-500
Usage: fuzz [-h] [--only-new-bugs] [--quiet] [--test-executable TEST_EXECUTABLE] [--baseline-executable BASELINE_EXECUTABLE] --bin {ruff,ty} seeds [seeds ...]
fuzz: error: Bad argument passed to `--baseline-executable`: no such file or executable PosixPath('ty_main')
 "Bad argument passed to `--baseline-executable`: no such file or executable PosixPath('ty_main')"
```

Using `.absolute()` on the `Path` fixes this.


## Test Plan

Successful `py-fuzzer` run with the invocation above.
2025-06-24 15:16:21 +02:00
Micha Reiser
833be2e66a [ty] Change environment.root to accept multiple paths (#18913) 2025-06-24 14:52:36 +02:00
Micha Reiser
0194452928 [ty] Rename src.root setting to environment.root (#18760)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-06-24 14:40:44 +02:00
Dhruv Manilawala
2c4c015f74 Use file path for detecting package root (#18914)
Ref: https://github.com/astral-sh/ruff/pull/18910#discussion_r2163847956
2025-06-24 12:32:41 +00:00
Dhruv Manilawala
66fc7c8fc0 Consider virtual path for various server actions (#18910)
## Summary

Ref:
https://github.com/astral-sh/ruff/issues/14820#issuecomment-2996690681

This PR fixes a bug where virtual paths or any paths that doesn't exists
on the file system weren't being considered for checking inclusion /
exclusion. This was because the logic used `file_path` which returns
`None` for those path. This PR fixes that by using the
`virtual_file_path` method that returns a `Path` corresponding to the
actual file on disk or any kind of virtual path.

This should ideally just fix the above linked issue by way of excluding
the documents representing the interactive window because they aren't in
the inclusion set. It failed only on Windows previously because the file
path construction would fail and then Ruff would default to including
all the files.

## Test Plan

On my machine, the `.interactive` paths are always excluded so I'm using
the inclusion set instead:

```json
{
  "ruff.nativeServer": "on",
  "ruff.path": ["/Users/dhruv/work/astral/ruff/target/debug/ruff"],
  "ruff.configuration": {
    "extend-include": ["*.interactive"]
  }
}
```

The diagnostics are shown for both the file paths and the interactive
window:

<img width="1727" alt="Screenshot 2025-06-24 at 14 56 40"
src="https://github.com/user-attachments/assets/d36af96a-777e-4367-8acf-4d9c9014d025"
/>

And, the logs:

```
2025-06-24 14:56:26.478275000 DEBUG notification{method="notebookDocument/didChange"}: Included path via `extend-include`: /Interactive-1.interactive
```

And, when using `ruff.exclude` via:

```json
{
	"ruff.exclude": ["*.interactive"]
}
```

With logs:

```
2025-06-24 14:58:41.117743000 DEBUG notification{method="notebookDocument/didChange"}: Ignored path via `exclude`: /Interactive-1.interactive
```
2025-06-24 12:24:28 +00:00
Alex Waygood
237a5821ba [ty] Introduce UnionType::try_from_elements and UnionType::try_map (#18911) 2025-06-24 12:09:02 +00:00
Alex Waygood
27eee5a1a8 [ty] Support narrowing on isinstance()/issubclass() if the second argument is a dynamic, intersection, union or typevar type (#18900) 2025-06-24 10:55:26 +00:00
med1844
fd2cc37f90 [ty] Add decorator check for implicit attribute assignments (#18587)
## Summary

Previously, the checks for implicit attribute assignments didn't
properly account for method decorators. This PR fixes that by:

- Adding a decorator check in `implicit_instance_attribute`. This allows
it to filter out methods with mismatching decorators when analyzing
attribute assignments.
- Adding attribute search for implicit class attributes: if an attribute
can't be found directly in the class body, the
`ClassLiteral::own_class_member` function will now search in
classmethods.
- Adding `staticmethod`: it has been added into `KnownClass` and
together with the new decorator check, it will no longer expose
attributes when the assignment target name is the same as the first
method name.

If accepted, it should fix https://github.com/astral-sh/ty/issues/205
and https://github.com/astral-sh/ty/issues/207.

## Test Plan

This is tested with existing mdtest suites and is able to get most of
the TODO marks for implicit assignments in classmethods and
staticmethods removed.

However, there's one specific test case I failed to figure out how to
correctly resolve:


b279508bdc/crates/ty_python_semantic/resources/mdtest/attributes.md (L754-L755)

I tried to add `instance_member().is_unbound()` check in this [else
branch](b279508bdc/crates/ty_python_semantic/src/types/infer.rs (L3299-L3301))
but it causes tests with class attributes defined in class body to fail.
While it's possible to implicitly add `ClassVar` to qualifiers to make
this assignment fail and keep everything else passing, it doesn't feel
like the right solution.
2025-06-24 11:42:10 +02:00
Victor Hugo Gomes
ca7933804e [ruff] Trigger RUF037 for empty string and byte strings (#18862) 2025-06-24 08:26:28 +02:00
300 changed files with 10004 additions and 8413 deletions

33
Cargo.lock generated
View File

@@ -930,35 +930,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_home"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -3046,16 +3023,6 @@ dependencies = [
"walkdir",
]
[[package]]
name = "ruff_python_resolver"
version = "0.0.0"
dependencies = [
"env_logger",
"insta",
"log",
"tempfile",
]
[[package]]
name = "ruff_python_semantic"
version = "0.0.0"

View File

@@ -75,7 +75,6 @@ dashmap = { version = "6.0.1" }
dir-test = { version = "0.4.0" }
dunce = { version = "1.0.5" }
drop_bomb = { version = "0.1.5" }
env_logger = { version = "0.11.0" }
etcetera = { version = "0.10.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }

View File

@@ -442,7 +442,7 @@ impl LintCacheData {
// Parse the kebab-case rule name into a `Rule`. This will fail for syntax errors, so
// this also serves to filter them out, but we shouldn't be caching files with syntax
// errors anyway.
.filter_map(|msg| Some((msg.noqa_code().and_then(|code| code.rule())?, msg)))
.filter_map(|msg| Some((msg.name().parse().ok()?, msg)))
.map(|(rule, msg)| {
// Make sure that all message use the same source file.
assert_eq!(

View File

@@ -17,6 +17,7 @@ fn command() -> Command {
command.arg("analyze");
command.arg("graph");
command.arg("--preview");
command.env_clear();
command
}
@@ -565,8 +566,8 @@ fn venv() -> Result<()> {
----- stderr -----
ruff failed
Cause: Invalid search path settings
Cause: Failed to discover the site-packages directory: Invalid `--python` argument `none`: does not point to a Python executable or a directory on disk
Cause: Invalid `--python` argument `none`: does not point to a Python executable or a directory on disk
Cause: No such file or directory (os error 2)
");
});

View File

@@ -352,7 +352,7 @@ impl DisplaySet<'_> {
// FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char`
// is. For now, just accept that sometimes the code line will be longer than
// desired.
let next = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1);
let next = char_width(ch).unwrap_or(1);
if taken + next > right - left {
was_cut_right = true;
break;
@@ -377,7 +377,7 @@ impl DisplaySet<'_> {
let left: usize = text
.chars()
.take(left)
.map(|ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1))
.map(|ch| char_width(ch).unwrap_or(1))
.sum();
let mut annotations = annotations.clone();
@@ -1393,6 +1393,7 @@ fn format_body<'m>(
let line_length: usize = line.len();
let line_range = (current_index, current_index + line_length);
let end_line_size = end_line.len();
body.push(DisplayLine::Source {
lineno: Some(current_line),
inline_marks: vec![],
@@ -1452,12 +1453,12 @@ fn format_body<'m>(
let annotation_start_col = line
[0..(start - line_start_index).min(line_length)]
.chars()
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0))
.map(|c| char_width(c).unwrap_or(0))
.sum::<usize>();
let mut annotation_end_col = line
[0..(end - line_start_index).min(line_length)]
.chars()
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0))
.map(|c| char_width(c).unwrap_or(0))
.sum::<usize>();
if annotation_start_col == annotation_end_col {
// At least highlight something
@@ -1499,7 +1500,7 @@ fn format_body<'m>(
let annotation_start_col = line
[0..(start - line_start_index).min(line_length)]
.chars()
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0))
.map(|c| char_width(c).unwrap_or(0))
.sum::<usize>();
let annotation_end_col = annotation_start_col + 1;
@@ -1558,7 +1559,7 @@ fn format_body<'m>(
{
let end_mark = line[0..(end - line_start_index).min(line_length)]
.chars()
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0))
.map(|c| char_width(c).unwrap_or(0))
.sum::<usize>()
.saturating_sub(1);
// If the annotation ends on a line-end character, we
@@ -1754,3 +1755,11 @@ fn format_inline_marks(
}
Ok(())
}
fn char_width(c: char) -> Option<usize> {
if c == '\t' {
Some(4)
} else {
unicode_width::UnicodeWidthChar::width(c)
}
}

View File

@@ -0,0 +1,36 @@
<svg width="1196px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {
padding: 0 10px;
line-height: 18px;
}
.bold { font-weight: bold; }
tspan {
font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
white-space: pre;
line-height: 18px;
}
</style>
<rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
<text xml:space="preserve" class="container fg">
<tspan x="10px" y="28px"><tspan class="fg-bright-red bold">error[E0308]</tspan><tspan>: </tspan><tspan class="bold">mismatched types</tspan>
</tspan>
<tspan x="10px" y="46px"><tspan> </tspan><tspan class="fg-bright-blue bold">--&gt;</tspan><tspan> $DIR/non-whitespace-trimming.rs:4:6</tspan>
</tspan>
<tspan x="10px" y="64px"><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan>
</tspan>
<tspan x="10px" y="82px"><tspan class="fg-bright-blue bold">4 |</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">...</tspan><tspan> s_data['d_dails'] = bb['contacted'][hostip]['ansible_facts']['ansible_devices']['vda']['vendor'] + " " + bb['contacted'][hostip</tspan><tspan class="fg-bright-blue bold">...</tspan>
</tspan>
<tspan x="10px" y="100px"><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan> </tspan><tspan class="fg-bright-red bold">^^^^^^</tspan>
</tspan>
<tspan x="10px" y="118px"><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan>
</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,20 @@
[message]
level = "Error"
id = "E0308"
title = "mismatched types"
[[message.snippets]]
source = """
s_data['d_dails'] = bb['contacted'][hostip]['ansible_facts']['ansible_devices']['vda']['vendor'] + " " + bb['contacted'][hostip]['an
"""
line_start = 4
origin = "$DIR/non-whitespace-trimming.rs"
[[message.snippets.annotations]]
label = ""
level = "Error"
range = [5, 11]
[renderer]
# anonymized_line_numbers = true
color = true

View File

@@ -21,7 +21,7 @@
<text xml:space="preserve" class="container fg">
<tspan x="10px" y="28px"><tspan class="fg-bright-red bold">error[E0308]</tspan><tspan>: </tspan><tspan class="bold">mismatched types</tspan>
</tspan>
<tspan x="10px" y="46px"><tspan> </tspan><tspan class="fg-bright-blue bold">--&gt;</tspan><tspan> $DIR/non-whitespace-trimming.rs:4:242</tspan>
<tspan x="10px" y="46px"><tspan> </tspan><tspan class="fg-bright-blue bold">--&gt;</tspan><tspan> $DIR/non-whitespace-trimming.rs:4:238</tspan>
</tspan>
<tspan x="10px" y="64px"><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan>
</tspan>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -13,12 +13,12 @@ origin = "$DIR/non-whitespace-trimming.rs"
[[message.snippets.annotations]]
label = "expected `()`, found integer"
level = "Error"
range = [241, 243]
range = [237, 239]
[[message.snippets.annotations]]
label = "expected due to this"
level = "Error"
range = [236, 238]
range = [232, 234]
[renderer]

View File

@@ -348,6 +348,58 @@ fn benchmark_many_tuple_assignments(criterion: &mut Criterion) {
});
}
fn benchmark_many_attribute_assignments(criterion: &mut Criterion) {
setup_rayon();
criterion.bench_function("ty_micro[many_attribute_assignments]", |b| {
b.iter_batched_ref(
|| {
// This is a regression benchmark for https://github.com/astral-sh/ty/issues/627.
// Before this was fixed, the following sample would take >1s to type check.
setup_micro_case(
r#"
class C:
def f(self: "C"):
if isinstance(self.a, str):
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
if isinstance(self.b, str):
return
"#,
)
},
|case| {
let Case { db, .. } = case;
let result = db.check();
assert!(!result.is_empty());
},
BatchSize::SmallInput,
);
});
}
struct ProjectBenchmark<'a> {
project: InstalledProject<'a>,
fs: MemoryFileSystem,
@@ -377,8 +429,7 @@ impl<'a> ProjectBenchmark<'a> {
metadata.apply_options(Options {
environment: Some(EnvironmentOptions {
python_version: Some(RangedValue::cli(self.project.config.python_version)),
python: (!self.project.config().dependencies.is_empty())
.then_some(RelativePathBuf::cli(SystemPath::new(".venv"))),
python: Some(RelativePathBuf::cli(SystemPath::new(".venv"))),
..EnvironmentOptions::default()
}),
..Options::default()
@@ -483,6 +534,7 @@ criterion_group!(
micro,
benchmark_many_string_assignments,
benchmark_many_tuple_assignments,
benchmark_many_attribute_assignments,
);
criterion_group!(project, anyio, attrs, hydra);
criterion_main!(check_file, micro, project);

View File

@@ -36,8 +36,7 @@ impl<'a> Benchmark<'a> {
metadata.apply_options(Options {
environment: Some(EnvironmentOptions {
python_version: Some(RangedValue::cli(self.project.config.python_version)),
python: (!self.project.config().dependencies.is_empty())
.then_some(RelativePathBuf::cli(SystemPath::new(".venv"))),
python: Some(RelativePathBuf::cli(SystemPath::new(".venv"))),
..EnvironmentOptions::default()
}),
..Options::default()

View File

@@ -74,19 +74,17 @@ impl<'a> RealWorldProject<'a> {
};
// Install dependencies if specified
if !checkout.project().dependencies.is_empty() {
tracing::debug!(
"Installing {} dependencies for project '{}'...",
checkout.project().dependencies.len(),
checkout.project().name
);
let start = std::time::Instant::now();
install_dependencies(&checkout)?;
tracing::debug!(
"Dependency installation completed in {:.2}s",
start.elapsed().as_secs_f64()
);
}
tracing::debug!(
"Installing {} dependencies for project '{}'...",
checkout.project().dependencies.len(),
checkout.project().name
);
let start_install = std::time::Instant::now();
install_dependencies(&checkout)?;
tracing::debug!(
"Dependency installation completed in {:.2}s",
start_install.elapsed().as_secs_f64()
);
tracing::debug!("Project setup took: {:.2}s", start.elapsed().as_secs_f64());
@@ -281,6 +279,14 @@ fn install_dependencies(checkout: &Checkout) -> Result<()> {
String::from_utf8_lossy(&output.stderr)
);
if checkout.project().dependencies.is_empty() {
tracing::debug!(
"No dependencies to install for project '{}'",
checkout.project().name
);
return Ok(());
}
// Install dependencies with date constraint in the isolated environment
let mut cmd = Command::new("uv");
cmd.args([

View File

@@ -30,14 +30,14 @@ filetime = { workspace = true }
glob = { workspace = true }
ignore = { workspace = true, optional = true }
matchit = { workspace = true }
path-slash = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
path-slash = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, optional = true }
rustc-hash = { workspace = true }
zip = { workspace = true }
[target.'cfg(target_arch="wasm32")'.dependencies]

View File

@@ -735,6 +735,9 @@ pub enum DiagnosticId {
/// # no `[overrides.rules]`
/// ```
UselessOverridesSection,
/// Use of a deprecated setting.
DeprecatedSetting,
}
impl DiagnosticId {
@@ -773,6 +776,7 @@ impl DiagnosticId {
DiagnosticId::EmptyInclude => "empty-include",
DiagnosticId::UnnecessaryOverridesSection => "unnecessary-overrides-section",
DiagnosticId::UselessOverridesSection => "useless-overrides-section",
DiagnosticId::DeprecatedSetting => "deprecated-setting",
}
}

View File

@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Context, Result};
use std::sync::Arc;
use zip::CompressionMethod;
@@ -9,7 +9,7 @@ use ruff_db::{Db as SourceDb, Upcast};
use ruff_python_ast::PythonVersion;
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{
Db, Program, ProgramSettings, PythonPath, PythonPlatform, PythonVersionSource,
Db, Program, ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionSource,
PythonVersionWithSource, SearchPathSettings, SysPrefixPathOrigin, default_lint_registry,
};
@@ -35,24 +35,32 @@ impl ModuleDb {
python_version: PythonVersion,
venv_path: Option<SystemPathBuf>,
) -> Result<Self> {
let mut search_paths = SearchPathSettings::new(src_roots);
if let Some(venv_path) = venv_path {
search_paths.python_path =
PythonPath::sys_prefix(venv_path, SysPrefixPathOrigin::PythonCliFlag);
}
let db = Self::default();
let mut search_paths = SearchPathSettings::new(src_roots);
// TODO: Consider calling `PythonEnvironment::discover` if the `venv_path` is not provided.
if let Some(venv_path) = venv_path {
let environment =
PythonEnvironment::new(venv_path, SysPrefixPathOrigin::PythonCliFlag, db.system())?;
search_paths.site_packages_paths = environment
.site_packages_paths(db.system())
.context("Failed to discover the site-packages directory")?
.into_vec();
}
let search_paths = search_paths
.to_search_paths(db.system(), db.vendored())
.context("Invalid search path settings")?;
Program::from_settings(
&db,
ProgramSettings {
python_version: Some(PythonVersionWithSource {
python_version: PythonVersionWithSource {
version: python_version,
source: PythonVersionSource::default(),
}),
},
python_platform: PythonPlatform::default(),
search_paths,
},
)?;
);
Ok(db)
}

View File

@@ -0,0 +1,6 @@
def f_byte():
raise RuntimeError(b"This is an example exception")
def f_byte_empty():
raise RuntimeError(b"")

View File

@@ -2,13 +2,81 @@ import os.path
from pathlib import Path
from os.path import getsize
filename = "filename"
filename1 = __file__
filename2 = Path("filename")
os.path.getsize("filename")
os.path.getsize(b"filename")
os.path.getsize(Path("filename"))
os.path.getsize(__file__)
os.path.getsize(filename)
os.path.getsize(filename1)
os.path.getsize(filename2)
os.path.getsize(filename="filename")
os.path.getsize(filename=b"filename")
os.path.getsize(filename=Path("filename"))
os.path.getsize(filename=__file__)
getsize("filename")
getsize(b"filename")
getsize(Path("filename"))
getsize(__file__)
getsize(filename="filename")
getsize(filename=b"filename")
getsize(filename=Path("filename"))
getsize(filename=__file__)
getsize(filename)
getsize(filename1)
getsize(filename2)
os.path.getsize(
"filename", # comment
)
os.path.getsize(
# comment
"filename"
,
# comment
)
os.path.getsize(
# comment
b"filename"
# comment
)
os.path.getsize( # comment
Path(__file__)
# comment
) # comment
getsize( # comment
"filename")
getsize( # comment
b"filename",
#comment
)
os.path.getsize("file" + "name")
getsize \
\
\
( # comment
"filename",
)
getsize(Path("filename").resolve())
import pathlib
os.path.getsize(pathlib.Path("filename"))

View File

@@ -0,0 +1,5 @@
import os
os.path.getsize(filename="filename")
os.path.getsize(filename=b"filename")
os.path.getsize(filename=__file__)

View File

@@ -163,4 +163,10 @@ def foo():
for k, v in src:
if lambda: 0:
dst[k] = v
dst[k] = v
# https://github.com/astral-sh/ruff/issues/18859
def foo():
v = {}
for o,(x,)in():
v[x,]=o

View File

@@ -14,3 +14,6 @@ hidden = {"a": "!"}
"" % {
'test1': '',
'test2': '',
}
# https://github.com/astral-sh/ruff/issues/18806

View File

@@ -4,3 +4,11 @@
"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
(''
.format(x=2)) # F522
# https://github.com/astral-sh/ruff/issues/18806
# The fix here is unsafe because the unused argument has side effect
"Hello, {name}".format(greeting=print(1), name="World")
# The fix here is safe because the unused argument has no side effect,
# even though the used argument has a side effect
"Hello, {name}".format(greeting="Pikachu", name=print(1))

View File

@@ -35,3 +35,11 @@
# Removing the final argument.
"Hello".format("world")
"Hello".format("world", key="value")
# https://github.com/astral-sh/ruff/issues/18806
# The fix here is unsafe because the unused argument has side effect
"Hello, {0}".format("world", print(1))
# The fix here is safe because the unused argument has no side effect,
# even though the used argument has a side effect
"Hello, {0}".format(print(1), "Pikachu")

View File

@@ -1,3 +1,5 @@
# Mock objects
# ============
# Errors
assert my_mock.not_called()
assert my_mock.called_once_with()
@@ -17,3 +19,25 @@ my_mock.assert_called()
my_mock.assert_called_once_with()
"""like :meth:`Mock.assert_called_once_with`"""
"""like :meth:`MagicMock.assert_called_once_with`"""
# AsyncMock objects
# =================
# Errors
assert my_mock.not_awaited()
assert my_mock.awaited_once_with()
assert my_mock.not_awaited
assert my_mock.awaited_once_with
my_mock.assert_not_awaited
my_mock.assert_awaited
my_mock.assert_awaited_once_with
my_mock.assert_awaited_once_with
MyMock.assert_awaited_once_with
assert my_mock.awaited
# OK
assert my_mock.await_count == 1
my_mock.assert_not_awaited()
my_mock.assert_awaited()
my_mock.assert_awaited_once_with()
"""like :meth:`Mock.assert_awaited_once_with`"""
"""like :meth:`MagicMock.assert_awaited_once_with`"""

View File

@@ -14,7 +14,7 @@ min(1, min(2, 3, key=test))
# This will still trigger, to merge the calls without keyword args.
min(1, min(2, 3, key=test), min(4, 5))
# Don't provide a fix if there are comments within the call.
# The fix is already unsafe, so deleting comments is okay.
min(
1, # This is a comment.
min(2, 3),

View File

@@ -20,6 +20,12 @@ _ = Decimal.from_float(float("-inf"))
_ = Decimal.from_float(float("Infinity"))
_ = Decimal.from_float(float("-Infinity"))
_ = Decimal.from_float(float("nan"))
_ = Decimal.from_float(float("-NaN"))
_ = Decimal.from_float(float(" \n+nan \t"))
_ = Decimal.from_float(float(" iNf\n\t "))
_ = Decimal.from_float(float(" -inF\n \t"))
_ = Decimal.from_float(float(" InfinIty\n\t "))
_ = Decimal.from_float(float(" -InfinIty\n \t"))
# OK
_ = Fraction(0.1)

View File

@@ -36,5 +36,19 @@ f"{ascii(bla)}" # OK
)
# OK
# https://github.com/astral-sh/ruff/issues/16325
f"{str({})}"
f"{str({} | {})}"
import builtins
f"{builtins.repr(1)}"
f"{repr(1)=}"
f"{repr(lambda: 1)}"
f"{repr(x := 2)}"
f"{str(object=3)}"

View File

@@ -1,5 +1,5 @@
from collections import deque
import collections
from collections import deque
def f():
@@ -91,3 +91,14 @@ def f():
def f():
deque([], iterable=[])
# https://github.com/astral-sh/ruff/issues/18854
deque("")
deque(b"")
deque(f"")
deque(f"" "")
deque(f"" f"")
deque("abc") # OK
deque(b"abc") # OK
deque(f"" "a") # OK
deque(f"{x}" "") # OK

View File

@@ -1,12 +1,7 @@
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Binding, BindingKind, Imported, ResolvedReference, ScopeKind};
use ruff_text_size::Ranged;
use rustc_hash::FxHashMap;
use ruff_python_semantic::{Binding, ScopeKind};
use crate::Fix;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::fix;
use crate::rules::{
flake8_builtins, flake8_pyi, flake8_type_checking, flake8_unused_arguments, pep8_naming,
pyflakes, pylint, ruff,
@@ -94,260 +89,19 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
}
if checker.is_rule_enabled(Rule::GlobalVariableNotAssigned) {
for (name, binding_id) in scope.bindings() {
let binding = checker.semantic.binding(binding_id);
// If the binding is a `global`, then it's a top-level `global` that was never
// assigned in the current scope. If it were assigned, the `global` would be
// shadowed by the assignment.
if binding.kind.is_global() {
// If the binding was conditionally deleted, it will include a reference within
// a `Del` context, but won't be shadowed by a `BindingKind::Deletion`, as in:
// ```python
// if condition:
// del var
// ```
if binding
.references
.iter()
.map(|id| checker.semantic.reference(*id))
.all(ResolvedReference::is_load)
{
checker.report_diagnostic(
pylint::rules::GlobalVariableNotAssigned {
name: (*name).to_string(),
},
binding.range(),
);
}
}
}
pylint::rules::global_variable_not_assigned(checker, scope);
}
if checker.is_rule_enabled(Rule::RedefinedArgumentFromLocal) {
for (name, binding_id) in scope.bindings() {
for shadow in checker.semantic.shadowed_bindings(scope_id, binding_id) {
let binding = &checker.semantic.bindings[shadow.binding_id()];
if !matches!(
binding.kind,
BindingKind::LoopVar
| BindingKind::BoundException
| BindingKind::WithItemVar
) {
continue;
}
let shadowed = &checker.semantic.bindings[shadow.shadowed_id()];
if !shadowed.kind.is_argument() {
continue;
}
if checker.settings().dummy_variable_rgx.is_match(name) {
continue;
}
let scope = &checker.semantic.scopes[binding.scope];
if scope.kind.is_generator() {
continue;
}
checker.report_diagnostic(
pylint::rules::RedefinedArgumentFromLocal {
name: name.to_string(),
},
binding.range(),
);
}
}
pylint::rules::redefined_argument_from_local(checker, scope_id, scope);
}
if checker.is_rule_enabled(Rule::ImportShadowedByLoopVar) {
for (name, binding_id) in scope.bindings() {
for shadow in checker.semantic.shadowed_bindings(scope_id, binding_id) {
// If the shadowing binding isn't a loop variable, abort.
let binding = &checker.semantic.bindings[shadow.binding_id()];
if !binding.kind.is_loop_var() {
continue;
}
// If the shadowed binding isn't an import, abort.
let shadowed = &checker.semantic.bindings[shadow.shadowed_id()];
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) {
continue;
}
// If the bindings are in different forks, abort.
if shadowed.source.is_none_or(|left| {
binding
.source
.is_none_or(|right| !checker.semantic.same_branch(left, right))
}) {
continue;
}
checker.report_diagnostic(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
row: checker.compute_source_row(shadowed.start()),
},
binding.range(),
);
}
}
pyflakes::rules::import_shadowed_by_loop_var(checker, scope_id, scope);
}
if checker.is_rule_enabled(Rule::RedefinedWhileUnused) {
// Index the redefined bindings by statement.
let mut redefinitions = FxHashMap::default();
for (name, binding_id) in scope.bindings() {
for shadow in checker.semantic.shadowed_bindings(scope_id, binding_id) {
// If the shadowing binding is a loop variable, abort, to avoid overlap
// with F402.
let binding = &checker.semantic.bindings[shadow.binding_id()];
if binding.kind.is_loop_var() {
continue;
}
// If the shadowed binding is used, abort.
let shadowed = &checker.semantic.bindings[shadow.shadowed_id()];
if shadowed.is_used() {
continue;
}
// If the shadowing binding isn't considered a "redefinition" of the
// shadowed binding, abort.
if !binding.redefines(shadowed) {
continue;
}
if shadow.same_scope() {
// If the symbol is a dummy variable, abort, unless the shadowed
// binding is an import.
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) && checker.settings().dummy_variable_rgx.is_match(name)
{
continue;
}
let Some(node_id) = shadowed.source else {
continue;
};
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition() {
if checker
.semantic
.statement(node_id)
.as_function_def_stmt()
.is_some_and(|function| {
visibility::is_overload(
&function.decorator_list,
&checker.semantic,
)
})
{
continue;
}
}
} else {
// Only enforce cross-scope shadowing for imports.
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) {
continue;
}
}
// If the bindings are in different forks, abort.
if shadowed.source.is_none_or(|left| {
binding
.source
.is_none_or(|right| !checker.semantic.same_branch(left, right))
}) {
continue;
}
redefinitions
.entry(binding.source)
.or_insert_with(Vec::new)
.push((shadowed, binding));
}
}
// Create a fix for each source statement.
let mut fixes = FxHashMap::default();
for (source, entries) in &redefinitions {
let Some(source) = source else {
continue;
};
let member_names = entries
.iter()
.filter_map(|(shadowed, binding)| {
if let Some(shadowed_import) = shadowed.as_any_import() {
if let Some(import) = binding.as_any_import() {
if shadowed_import.qualified_name() == import.qualified_name() {
return Some(import.member_name());
}
}
}
None
})
.collect::<Vec<_>>();
if !member_names.is_empty() {
let statement = checker.semantic.statement(*source);
let parent = checker.semantic.parent_statement(*source);
let Ok(edit) = fix::edits::remove_unused_imports(
member_names.iter().map(std::convert::AsRef::as_ref),
statement,
parent,
checker.locator(),
checker.stylist(),
checker.indexer(),
) else {
continue;
};
fixes.insert(
*source,
Fix::safe_edit(edit).isolate(Checker::isolation(
checker.semantic().parent_statement_id(*source),
)),
);
}
}
// Create diagnostics for each statement.
for (source, entries) in &redefinitions {
for (shadowed, binding) in entries {
let mut diagnostic = checker.report_diagnostic(
pyflakes::rules::RedefinedWhileUnused {
name: binding.name(checker.source()).to_string(),
row: checker.compute_source_row(shadowed.start()),
},
binding.range(),
);
if let Some(range) = binding.parent_range(&checker.semantic) {
diagnostic.set_parent(range.start());
}
if let Some(fix) = source.as_ref().and_then(|source| fixes.get(source)) {
diagnostic.set_fix(fix.clone());
}
}
}
pyflakes::rules::redefined_while_unused(checker, scope_id, scope);
}
if checker.source_type.is_stub()

View File

@@ -539,14 +539,13 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
let location = expr.range();
match pyflakes::format::FormatSummary::try_from(string_value.to_str()) {
Err(e) => {
if checker.is_rule_enabled(Rule::StringDotFormatInvalidFormat) {
checker.report_diagnostic(
pyflakes::rules::StringDotFormatInvalidFormat {
message: pyflakes::format::error_to_string(&e),
},
location,
);
}
// F521
checker.report_diagnostic_if_enabled(
pyflakes::rules::StringDotFormatInvalidFormat {
message: pyflakes::format::error_to_string(&e),
},
location,
);
}
Ok(summary) => {
if checker
@@ -1056,7 +1055,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
Rule::OsPathSplitext,
Rule::BuiltinOpen,
Rule::PyPath,
Rule::OsPathGetsize,
Rule::OsPathGetatime,
Rule::OsPathGetmtime,
Rule::OsPathGetctime,
@@ -1066,6 +1064,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
]) {
flake8_use_pathlib::rules::replaceable_by_pathlib(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathGetsize) {
flake8_use_pathlib::rules::os_path_getsize(checker, call);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(checker, call);
}
@@ -1313,26 +1314,22 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
typ: CFormatErrorType::UnsupportedFormatChar(c),
..
}) => {
if checker
.is_rule_enabled(Rule::PercentFormatUnsupportedFormatCharacter)
{
checker.report_diagnostic(
pyflakes::rules::PercentFormatUnsupportedFormatCharacter {
char: c,
},
location,
);
}
// F509
checker.report_diagnostic_if_enabled(
pyflakes::rules::PercentFormatUnsupportedFormatCharacter {
char: c,
},
location,
);
}
Err(e) => {
if checker.is_rule_enabled(Rule::PercentFormatInvalidFormat) {
checker.report_diagnostic(
pyflakes::rules::PercentFormatInvalidFormat {
message: e.to_string(),
},
location,
);
}
// F501
checker.report_diagnostic_if_enabled(
pyflakes::rules::PercentFormatInvalidFormat {
message: e.to_string(),
},
location,
);
}
Ok(summary) => {
if checker.is_rule_enabled(Rule::PercentFormatExpectedMapping) {

View File

@@ -45,18 +45,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.is_rule_enabled(Rule::NonlocalWithoutBinding) {
if !checker.semantic.scope_id.is_global() {
for name in names {
if checker.semantic.nonlocal(name).is_none() {
checker.report_diagnostic(
pylint::rules::NonlocalWithoutBinding {
name: name.to_string(),
},
name.range(),
);
}
}
}
pylint::rules::nonlocal_without_binding(checker, nonlocal);
}
if checker.is_rule_enabled(Rule::NonlocalAndGlobal) {
pylint::rules::nonlocal_and_global(checker, nonlocal);
@@ -828,6 +817,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pyflakes::rules::future_feature_not_defined(checker, alias);
}
} else if &alias.name == "*" {
// F406
if checker.is_rule_enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
checker.report_diagnostic(
@@ -838,14 +828,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
);
}
}
if checker.is_rule_enabled(Rule::UndefinedLocalWithImportStar) {
checker.report_diagnostic(
pyflakes::rules::UndefinedLocalWithImportStar {
name: helpers::format_import_from(level, module).to_string(),
},
stmt.range(),
);
}
// F403
checker.report_diagnostic_if_enabled(
pyflakes::rules::UndefinedLocalWithImportStar {
name: helpers::format_import_from(level, module).to_string(),
},
stmt.range(),
);
}
if checker.is_rule_enabled(Rule::RelativeImports) {
flake8_tidy_imports::rules::banned_relative_import(

View File

@@ -13,15 +13,15 @@ pub(crate) fn unresolved_references(checker: &Checker) {
for reference in checker.semantic.unresolved_references() {
if reference.is_wildcard_import() {
if checker.is_rule_enabled(Rule::UndefinedLocalWithImportStarUsage) {
checker.report_diagnostic(
pyflakes::rules::UndefinedLocalWithImportStarUsage {
name: reference.name(checker.source()).to_string(),
},
reference.range(),
);
}
// F406
checker.report_diagnostic_if_enabled(
pyflakes::rules::UndefinedLocalWithImportStarUsage {
name: reference.name(checker.source()).to_string(),
},
reference.range(),
);
} else {
// F821
if checker.is_rule_enabled(Rule::UndefinedName) {
if checker.semantic.in_no_type_check() {
continue;

View File

@@ -28,7 +28,7 @@ use itertools::Itertools;
use log::debug;
use rustc_hash::{FxHashMap, FxHashSet};
use ruff_diagnostics::IsolationLevel;
use ruff_diagnostics::{Applicability, Fix, IsolationLevel};
use ruff_notebook::{CellOffsets, NotebookIndex};
use ruff_python_ast::helpers::{collect_import_from_member, is_docstring_stmt, to_module_path};
use ruff_python_ast::identifier::Identifier;
@@ -388,7 +388,7 @@ impl<'a> Checker<'a> {
/// Return a [`DiagnosticGuard`] for reporting a diagnostic.
///
/// The guard derefs to a [`Diagnostic`], so it can be used to further modify the diagnostic
/// The guard derefs to an [`OldDiagnostic`], so it can be used to further modify the diagnostic
/// before it is added to the collection in the checker on `Drop`.
pub(crate) fn report_diagnostic<'chk, T: Violation>(
&'chk self,
@@ -401,8 +401,8 @@ impl<'a> Checker<'a> {
/// Return a [`DiagnosticGuard`] for reporting a diagnostic if the corresponding rule is
/// enabled.
///
/// Prefer [`Checker::report_diagnostic`] in general because the conversion from a `Diagnostic`
/// to a `Rule` is somewhat expensive.
/// The guard derefs to an [`OldDiagnostic`], so it can be used to further modify the diagnostic
/// before it is added to the collection in the checker on `Drop`.
pub(crate) fn report_diagnostic_if_enabled<'chk, T: Violation>(
&'chk self,
kind: T,
@@ -625,6 +625,7 @@ impl SemanticSyntaxContext for Checker<'_> {
fn report_semantic_error(&self, error: SemanticSyntaxError) {
match error.kind {
SemanticSyntaxErrorKind::LateFutureImport => {
// F404
if self.is_rule_enabled(Rule::LateFutureImport) {
self.report_diagnostic(LateFutureImport, error.range);
}
@@ -646,6 +647,7 @@ impl SemanticSyntaxContext for Checker<'_> {
}
}
SemanticSyntaxErrorKind::ReturnOutsideFunction => {
// F706
if self.is_rule_enabled(Rule::ReturnOutsideFunction) {
self.report_diagnostic(ReturnOutsideFunction, error.range);
}
@@ -2808,6 +2810,7 @@ impl<'a> Checker<'a> {
Err(parse_error) => {
self.semantic.restore(snapshot);
// F722
if self.is_rule_enabled(Rule::ForwardAnnotationSyntaxError) {
self.report_type_diagnostic(
pyflakes::rules::ForwardAnnotationSyntaxError {
@@ -2955,6 +2958,7 @@ impl<'a> Checker<'a> {
self.semantic.flags -= SemanticModelFlags::DUNDER_ALL_DEFINITION;
} else {
if self.semantic.global_scope().uses_star_imports() {
// F405
if self.is_rule_enabled(Rule::UndefinedLocalWithImportStarUsage) {
self.report_diagnostic(
pyflakes::rules::UndefinedLocalWithImportStarUsage {
@@ -2965,6 +2969,7 @@ impl<'a> Checker<'a> {
.set_parent(definition.start());
}
} else {
// F822
if self.is_rule_enabled(Rule::UndefinedExport) {
if is_undefined_export_in_dunder_init_enabled(self.settings())
|| !self.path.ends_with("__init__.py")
@@ -3145,7 +3150,7 @@ impl<'a> LintContext<'a> {
/// Return a [`DiagnosticGuard`] for reporting a diagnostic.
///
/// The guard derefs to an [`OldDiagnostic`], so it can be used to further modify the diagnostic
/// before it is added to the collection in the collector on `Drop`.
/// before it is added to the collection in the context on `Drop`.
pub(crate) fn report_diagnostic<'chk, T: Violation>(
&'chk self,
kind: T,
@@ -3154,23 +3159,26 @@ impl<'a> LintContext<'a> {
DiagnosticGuard {
context: self,
diagnostic: Some(OldDiagnostic::new(kind, range, &self.source_file)),
rule: T::rule(),
}
}
/// Return a [`DiagnosticGuard`] for reporting a diagnostic if the corresponding rule is
/// enabled.
///
/// Prefer [`DiagnosticsCollector::report_diagnostic`] in general because the conversion from an
/// `OldDiagnostic` to a `Rule` is somewhat expensive.
/// The guard derefs to an [`OldDiagnostic`], so it can be used to further modify the diagnostic
/// before it is added to the collection in the context on `Drop`.
pub(crate) fn report_diagnostic_if_enabled<'chk, T: Violation>(
&'chk self,
kind: T,
range: TextRange,
) -> Option<DiagnosticGuard<'chk, 'a>> {
if self.is_rule_enabled(T::rule()) {
let rule = T::rule();
if self.is_rule_enabled(rule) {
Some(DiagnosticGuard {
context: self,
diagnostic: Some(OldDiagnostic::new(kind, range, &self.source_file)),
rule,
})
} else {
None
@@ -3222,6 +3230,7 @@ pub(crate) struct DiagnosticGuard<'a, 'b> {
///
/// This is always `Some` until the `Drop` (or `defuse`) call.
diagnostic: Option<OldDiagnostic>,
rule: Rule,
}
impl DiagnosticGuard<'_, '_> {
@@ -3234,6 +3243,50 @@ impl DiagnosticGuard<'_, '_> {
}
}
impl DiagnosticGuard<'_, '_> {
fn resolve_applicability(&self, fix: &Fix) -> Applicability {
self.context
.settings
.fix_safety
.resolve_applicability(self.rule, fix.applicability())
}
/// Set the [`Fix`] used to fix the diagnostic.
#[inline]
pub(crate) fn set_fix(&mut self, fix: Fix) {
if !self.context.rules.should_fix(self.rule) {
self.fix = None;
return;
}
let applicability = self.resolve_applicability(&fix);
self.fix = Some(fix.with_applicability(applicability));
}
/// Set the [`Fix`] used to fix the diagnostic, if the provided function returns `Ok`.
/// Otherwise, log the error.
#[inline]
pub(crate) fn try_set_fix(&mut self, func: impl FnOnce() -> anyhow::Result<Fix>) {
match func() {
Ok(fix) => self.set_fix(fix),
Err(err) => log::debug!("Failed to create fix for {}: {}", self.name(), err),
}
}
/// Set the [`Fix`] used to fix the diagnostic, if the provided function returns `Ok`.
/// Otherwise, log the error.
#[inline]
pub(crate) fn try_set_optional_fix(
&mut self,
func: impl FnOnce() -> anyhow::Result<Option<Fix>>,
) {
match func() {
Ok(None) => {}
Ok(Some(fix)) => self.set_fix(fix),
Err(err) => log::debug!("Failed to create fix for {}: {}", self.name(), err),
}
}
}
impl std::ops::Deref for DiagnosticGuard<'_, '_> {
type Target = OldDiagnostic;

View File

@@ -22,6 +22,7 @@ use crate::{Edit, Fix, Locator};
use super::ast::LintContext;
/// RUF100
pub(crate) fn check_noqa(
context: &mut LintContext,
path: &Path,

View File

@@ -3,8 +3,8 @@ use anyhow::{Result, bail};
use libcst_native::{
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FormattedString,
FormattedStringContent, FormattedStringExpression, FunctionDef, GeneratorExp, If, Import,
ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, ListComp, Module, Name,
SmallStatement, Statement, Suite, Tuple, With,
ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, ListComp, Module, SmallStatement,
Statement, Suite, Tuple, With,
};
use ruff_python_codegen::Stylist;
@@ -104,14 +104,6 @@ pub(crate) fn match_attribute<'a, 'b>(
}
}
pub(crate) fn match_name<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a Name<'b>> {
if let Expression::Name(name) = expression {
Ok(name)
} else {
bail!("Expected Expression::Name")
}
}
pub(crate) fn match_arg<'a, 'b>(call: &'a Call<'b>) -> Result<&'a Arg<'b>> {
if let Some(arg) = call.args.first() {
Ok(arg)

View File

@@ -749,15 +749,16 @@ x = 1 \
let diag = {
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
let mut iter = edits.into_iter();
OldDiagnostic::new(
let mut diagnostic = OldDiagnostic::new(
MissingNewlineAtEndOfFile, // The choice of rule here is arbitrary.
TextRange::default(),
&SourceFileBuilder::new("<filename>", "<code>").finish(),
)
.with_fix(Fix::safe_edits(
);
diagnostic.fix = Some(Fix::safe_edits(
iter.next().ok_or(anyhow!("expected edits nonempty"))?,
iter,
))
));
diagnostic
};
assert_eq!(apply_fixes([diag].iter(), &locator).code, expect);
Ok(())

View File

@@ -186,12 +186,13 @@ mod tests {
edit.into_iter()
.map(|edit| {
// The choice of rule here is arbitrary.
let diagnostic = OldDiagnostic::new(
let mut diagnostic = OldDiagnostic::new(
MissingNewlineAtEndOfFile,
edit.range(),
&SourceFileBuilder::new(filename, source).finish(),
);
diagnostic.with_fix(Fix::safe_edit(edit))
diagnostic.fix = Some(Fix::safe_edit(edit));
diagnostic
})
.collect()
}

View File

@@ -378,32 +378,7 @@ pub fn check_path(
let (mut diagnostics, source_file) = context.into_parts();
if parsed.has_valid_syntax() {
// Remove fixes for any rules marked as unfixable.
for diagnostic in &mut diagnostics {
if diagnostic
.noqa_code()
.and_then(|code| code.rule())
.is_none_or(|rule| !settings.rules.should_fix(rule))
{
diagnostic.fix = None;
}
}
// Update fix applicability to account for overrides
if !settings.fix_safety.is_empty() {
for diagnostic in &mut diagnostics {
if let Some(fix) = diagnostic.fix.take() {
if let Some(rule) = diagnostic.noqa_code().and_then(|code| code.rule()) {
let fixed_applicability = settings
.fix_safety
.resolve_applicability(rule, fix.applicability());
diagnostic.set_fix(fix.with_applicability(fixed_applicability));
}
}
}
}
} else {
if !parsed.has_valid_syntax() {
// Avoid fixing in case the source code contains syntax errors.
for diagnostic in &mut diagnostics {
diagnostic.fix = None;

View File

@@ -186,41 +186,6 @@ impl OldDiagnostic {
)
}
/// Consumes `self` and returns a new `Diagnostic` with the given `fix`.
#[inline]
#[must_use]
pub fn with_fix(mut self, fix: Fix) -> Self {
self.set_fix(fix);
self
}
/// Set the [`Fix`] used to fix the diagnostic.
#[inline]
pub fn set_fix(&mut self, fix: Fix) {
self.fix = Some(fix);
}
/// Set the [`Fix`] used to fix the diagnostic, if the provided function returns `Ok`.
/// Otherwise, log the error.
#[inline]
pub fn try_set_fix(&mut self, func: impl FnOnce() -> anyhow::Result<Fix>) {
match func() {
Ok(fix) => self.fix = Some(fix),
Err(err) => log::debug!("Failed to create fix for {}: {}", self.name(), err),
}
}
/// Set the [`Fix`] used to fix the diagnostic, if the provided function returns `Ok`.
/// Otherwise, log the error.
#[inline]
pub fn try_set_optional_fix(&mut self, func: impl FnOnce() -> anyhow::Result<Option<Fix>>) {
match func() {
Ok(None) => {}
Ok(Some(fix)) => self.fix = Some(fix),
Err(err) => log::debug!("Failed to create fix for {}: {}", self.name(), err),
}
}
/// Consumes `self` and returns a new `Diagnostic` with the given parent node.
#[inline]
#[must_use]

View File

@@ -50,6 +50,11 @@ pub(crate) const fn is_fix_manual_list_comprehension_enabled(settings: &LinterSe
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18763
pub(crate) const fn is_fix_os_path_getsize_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/11436
// https://github.com/astral-sh/ruff/pull/11168
pub(crate) const fn is_dunder_init_fix_unused_import_enabled(settings: &LinterSettings) -> bool {
@@ -84,3 +89,13 @@ pub(crate) const fn is_ignore_init_files_in_useless_alias_enabled(
) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18547
pub(crate) const fn is_invalid_async_mock_access_check_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18867
pub(crate) const fn is_raise_exception_byte_string_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -11,6 +11,7 @@ use crate::registry::Rule;
use crate::rules::ruff::rules::InvalidPyprojectToml;
use crate::settings::LinterSettings;
/// RUF200
pub fn lint_pyproject_toml(
source_file: &SourceFile,
settings: &LinterSettings,

View File

@@ -215,6 +215,12 @@ pub enum Linter {
}
pub trait RuleNamespace: Sized {
/// Returns the prefix that every single code that ruff uses to identify
/// rules from this linter starts with. In the case that multiple
/// `#[prefix]`es are configured for the variant in the `Linter` enum
/// definition this is the empty string.
fn common_prefix(&self) -> &'static str;
/// Attempts to parse the given rule code. If the prefix is recognized
/// returns the respective variant along with the code with the common
/// prefix stripped.

View File

@@ -265,6 +265,7 @@ mod schema {
use strum::IntoEnumIterator;
use crate::RuleSelector;
use crate::registry::RuleNamespace;
use crate::rule_selector::{Linter, RuleCodePrefix};
impl JsonSchema for RuleSelector {

View File

@@ -273,9 +273,7 @@ pub(crate) fn compare(checker: &Checker, left: &Expr, ops: &[CmpOp], comparators
],
) = (ops, comparators)
{
if checker.is_rule_enabled(Rule::SysVersionInfo1CmpInt) {
checker.report_diagnostic(SysVersionInfo1CmpInt, left.range());
}
checker.report_diagnostic_if_enabled(SysVersionInfo1CmpInt, left.range());
}
}
}
@@ -294,9 +292,7 @@ pub(crate) fn compare(checker: &Checker, left: &Expr, ops: &[CmpOp], comparators
],
) = (ops, comparators)
{
if checker.is_rule_enabled(Rule::SysVersionInfoMinorCmpInt) {
checker.report_diagnostic(SysVersionInfoMinorCmpInt, left.range());
}
checker.report_diagnostic_if_enabled(SysVersionInfoMinorCmpInt, left.range());
}
}
@@ -310,11 +306,9 @@ pub(crate) fn compare(checker: &Checker, left: &Expr, ops: &[CmpOp], comparators
) = (ops, comparators)
{
if value.len() == 1 {
if checker.is_rule_enabled(Rule::SysVersionCmpStr10) {
checker.report_diagnostic(SysVersionCmpStr10, left.range());
}
} else if checker.is_rule_enabled(Rule::SysVersionCmpStr3) {
checker.report_diagnostic(SysVersionCmpStr3, left.range());
checker.report_diagnostic_if_enabled(SysVersionCmpStr10, left.range());
} else {
checker.report_diagnostic_if_enabled(SysVersionCmpStr3, left.range());
}
}
}

View File

@@ -738,6 +738,7 @@ pub(crate) fn definition(
.suppress_none_returning
&& is_none_returning(body)
) {
// ANN206
if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) {
if checker.is_rule_enabled(Rule::MissingReturnTypeClassMethod) {
let return_type = if is_stub_function(function, checker) {
@@ -765,6 +766,7 @@ pub(crate) fn definition(
diagnostics.push(diagnostic);
}
} else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) {
// ANN205
if checker.is_rule_enabled(Rule::MissingReturnTypeStaticMethod) {
let return_type = if is_stub_function(function, checker) {
None
@@ -791,6 +793,7 @@ pub(crate) fn definition(
diagnostics.push(diagnostic);
}
} else if is_method && visibility::is_init(name) {
// ANN204
// Allow omission of return annotation in `__init__` functions, as long as at
// least one argument is typed.
if checker.is_rule_enabled(Rule::MissingReturnTypeSpecialMethod) {

View File

@@ -312,25 +312,21 @@ pub(crate) fn shell_injection(checker: &Checker, call: &ast::ExprCall) {
Some(ShellKeyword {
truthiness: truthiness @ (Truthiness::True | Truthiness::Truthy),
}) => {
if checker.is_rule_enabled(Rule::SubprocessPopenWithShellEqualsTrue) {
checker.report_diagnostic(
SubprocessPopenWithShellEqualsTrue {
safety: Safety::from(arg),
is_exact: matches!(truthiness, Truthiness::True),
},
call.func.range(),
);
}
checker.report_diagnostic_if_enabled(
SubprocessPopenWithShellEqualsTrue {
safety: Safety::from(arg),
is_exact: matches!(truthiness, Truthiness::True),
},
call.func.range(),
);
}
// S603
_ => {
if !is_trusted_input(arg) {
if checker.is_rule_enabled(Rule::SubprocessWithoutShellEqualsTrue) {
checker.report_diagnostic(
SubprocessWithoutShellEqualsTrue,
call.func.range(),
);
}
checker.report_diagnostic_if_enabled(
SubprocessWithoutShellEqualsTrue,
call.func.range(),
);
}
}
}
@@ -340,14 +336,12 @@ pub(crate) fn shell_injection(checker: &Checker, call: &ast::ExprCall) {
}) = shell_keyword
{
// S604
if checker.is_rule_enabled(Rule::CallWithShellEqualsTrue) {
checker.report_diagnostic(
CallWithShellEqualsTrue {
is_exact: matches!(truthiness, Truthiness::True),
},
call.func.range(),
);
}
checker.report_diagnostic_if_enabled(
CallWithShellEqualsTrue {
is_exact: matches!(truthiness, Truthiness::True),
},
call.func.range(),
);
}
// S605

View File

@@ -51,6 +51,7 @@ impl Violation for BooleanPositionalValueInCall {
}
}
/// FBT003
pub(crate) fn boolean_positional_value_in_call(checker: &Checker, call: &ast::ExprCall) {
if allow_boolean_trap(call, checker) {
return;

View File

@@ -46,7 +46,7 @@ impl Violation for StaticKeyDictComprehension {
}
}
/// RUF011
/// B035, RUF011
pub(crate) fn static_key_dict_comprehension(checker: &Checker, dict_comp: &ast::ExprDictComp) {
// Collect the bound names in the comprehension's generators.
let names = {

View File

@@ -58,6 +58,7 @@ impl Violation for CallDateFromtimestamp {
}
}
/// DTZ012
pub(crate) fn call_date_fromtimestamp(checker: &Checker, func: &Expr, location: TextRange) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;

View File

@@ -57,6 +57,7 @@ impl Violation for CallDateToday {
}
}
/// DTZ011
pub(crate) fn call_date_today(checker: &Checker, func: &Expr, location: TextRange) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;

View File

@@ -69,6 +69,7 @@ impl Violation for CallDatetimeFromtimestamp {
}
}
/// DTZ006
pub(crate) fn call_datetime_fromtimestamp(checker: &Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;

View File

@@ -67,6 +67,7 @@ impl Violation for CallDatetimeNowWithoutTzinfo {
}
}
/// DTZ005
pub(crate) fn call_datetime_now_without_tzinfo(checker: &Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;

View File

@@ -56,6 +56,7 @@ impl Violation for CallDatetimeToday {
}
}
/// DTZ002
pub(crate) fn call_datetime_today(checker: &Checker, func: &Expr, location: TextRange) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;

View File

@@ -60,6 +60,7 @@ impl Violation for CallDatetimeUtcfromtimestamp {
}
}
/// DTZ004
pub(crate) fn call_datetime_utcfromtimestamp(checker: &Checker, func: &Expr, location: TextRange) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;

View File

@@ -63,6 +63,7 @@ impl Violation for CallDatetimeWithoutTzinfo {
}
}
/// DTZ001
pub(crate) fn call_datetime_without_tzinfo(checker: &Checker, call: &ast::ExprCall) {
if !checker.semantic().seen_module(Modules::DATETIME) {
return;

View File

@@ -46,6 +46,7 @@ impl Violation for Debugger {
}
}
/// T100
/// Checks for the presence of a debugger call.
pub(crate) fn debugger_call(checker: &Checker, expr: &Expr, func: &Expr) {
if let Some(using_type) =
@@ -64,6 +65,7 @@ pub(crate) fn debugger_call(checker: &Checker, expr: &Expr, func: &Expr) {
}
}
/// T100
/// Checks for the presence of a debugger import.
pub(crate) fn debugger_import(checker: &Checker, stmt: &Stmt, module: Option<&str>, name: &str) {
if let Some(module) = module {

View File

@@ -9,6 +9,7 @@ mod tests {
use anyhow::Result;
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_diagnostics, settings};
@@ -44,4 +45,17 @@ mod tests {
assert_diagnostics!("custom", diagnostics);
Ok(())
}
#[test]
fn preview_string_exception() -> Result<()> {
let diagnostics = test_path(
Path::new("flake8_errmsg/EM101_byte_string.py"),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(Rule::RawStringInException)
},
)?;
assert_diagnostics!("preview", diagnostics);
Ok(())
}
}

View File

@@ -7,12 +7,16 @@ use ruff_text_size::Ranged;
use crate::Locator;
use crate::checkers::ast::Checker;
use crate::preview::is_raise_exception_byte_string_enabled;
use crate::registry::Rule;
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for the use of string literals in exception constructors.
///
/// In [preview], this rule checks for byte string literals in
/// exception constructors.
///
/// ## Why is this bad?
/// Python includes the `raise` in the default traceback (and formatters
/// like Rich and IPython do too).
@@ -47,6 +51,8 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// raise RuntimeError(msg)
/// RuntimeError: 'Some value' is incorrect
/// ```
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
pub(crate) struct RawStringInException;
@@ -203,6 +209,28 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) {
}
}
}
// Check for byte string literals.
Expr::BytesLiteral(ast::ExprBytesLiteral { value: bytes, .. }) => {
if checker.settings().rules.enabled(Rule::RawStringInException) {
if bytes.len() >= checker.settings().flake8_errmsg.max_string_length
&& is_raise_exception_byte_string_enabled(checker.settings())
{
let mut diagnostic =
checker.report_diagnostic(RawStringInException, first.range());
if let Some(indentation) =
whitespace::indentation(checker.source(), stmt)
{
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
}
}
}
// Check for f-strings.
Expr::FString(_) => {
if checker.is_rule_enabled(Rule::FStringInException) {

View File

@@ -0,0 +1,35 @@
---
source: crates/ruff_linter/src/rules/flake8_errmsg/mod.rs
---
EM101_byte_string.py:2:24: EM101 [*] Exception must not use a string literal, assign to variable first
|
1 | def f_byte():
2 | raise RuntimeError(b"This is an example exception")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101
|
= help: Assign to variable; remove string literal
Unsafe fix
1 1 | def f_byte():
2 |- raise RuntimeError(b"This is an example exception")
2 |+ msg = b"This is an example exception"
3 |+ raise RuntimeError(msg)
3 4 |
4 5 |
5 6 | def f_byte_empty():
EM101_byte_string.py:6:24: EM101 [*] Exception must not use a string literal, assign to variable first
|
5 | def f_byte_empty():
6 | raise RuntimeError(b"")
| ^^^ EM101
|
= help: Assign to variable; remove string literal
Unsafe fix
3 3 |
4 4 |
5 5 | def f_byte_empty():
6 |- raise RuntimeError(b"")
6 |+ msg = b""
7 |+ raise RuntimeError(msg)

View File

@@ -1,9 +1,11 @@
use ruff_diagnostics::Fix;
use ruff_python_ast::Expr;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::{MemberNameImport, NameImport};
use ruff_text_size::Ranged;
use crate::Violation;
use crate::AlwaysFixableViolation;
use crate::checkers::ast::Checker;
/// ## What it does
@@ -61,6 +63,10 @@ use crate::checkers::ast::Checker;
/// def func(obj: dict[str, int | None]) -> None: ...
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as adding `from __future__ import annotations`
/// may change the semantics of the program.
///
/// ## Options
/// - `target-version`
#[derive(ViolationMetadata)]
@@ -68,12 +74,16 @@ pub(crate) struct FutureRewritableTypeAnnotation {
name: String,
}
impl Violation for FutureRewritableTypeAnnotation {
impl AlwaysFixableViolation for FutureRewritableTypeAnnotation {
#[derive_message_formats]
fn message(&self) -> String {
let FutureRewritableTypeAnnotation { name } = self;
format!("Add `from __future__ import annotations` to simplify `{name}`")
}
fn fix_title(&self) -> String {
"Add `from __future__ import annotations`".to_string()
}
}
/// FA100
@@ -83,7 +93,17 @@ pub(crate) fn future_rewritable_type_annotation(checker: &Checker, expr: &Expr)
.resolve_qualified_name(expr)
.map(|binding| binding.to_string());
if let Some(name) = name {
checker.report_diagnostic(FutureRewritableTypeAnnotation { name }, expr.range());
}
let Some(name) = name else { return };
let import = &NameImport::ImportFrom(MemberNameImport::member(
"__future__".to_string(),
"annotations".to_string(),
));
checker
.report_diagnostic(FutureRewritableTypeAnnotation { name }, expr.range())
.set_fix(Fix::unsafe_edit(
checker
.importer()
.add_import(import, ruff_text_size::TextSize::default()),
));
}

View File

@@ -1,19 +1,32 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
snapshot_kind: text
---
edge_case.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
edge_case.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List`
|
5 | def main(_: List[int]) -> None:
| ^^^^ FA100
6 | a_list: t.List[str] = []
7 | a_list.append("hello")
|
= help: Add `from __future__ import annotations`
edge_case.py:6:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
Unsafe fix
1 |+from __future__ import annotations
1 2 | from typing import List
2 3 | import typing as t
3 4 |
edge_case.py:6:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List`
|
5 | def main(_: List[int]) -> None:
6 | a_list: t.List[str] = []
| ^^^^^^ FA100
7 | a_list.append("hello")
|
= help: Add `from __future__ import annotations`
Unsafe fix
1 |+from __future__ import annotations
1 2 | from typing import List
2 3 | import typing as t
3 4 |

View File

@@ -1,11 +1,17 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
snapshot_kind: text
---
from_typing_import.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
from_typing_import.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List`
|
4 | def main() -> None:
5 | a_list: List[str] = []
| ^^^^ FA100
6 | a_list.append("hello")
|
= help: Add `from __future__ import annotations`
Unsafe fix
1 |+from __future__ import annotations
1 2 | from typing import List
2 3 |
3 4 |

View File

@@ -1,8 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
snapshot_kind: text
---
from_typing_import_many.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
from_typing_import_many.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List`
|
4 | def main() -> None:
5 | a_list: List[Optional[str]] = []
@@ -10,8 +9,15 @@ from_typing_import_many.py:5:13: FA100 Add `from __future__ import annotations`
6 | a_list.append("hello")
7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {})
|
= help: Add `from __future__ import annotations`
from_typing_import_many.py:5:18: FA100 Add `from __future__ import annotations` to simplify `typing.Optional`
Unsafe fix
1 |+from __future__ import annotations
1 2 | from typing import Dict, List, Optional, Set, Union, cast
2 3 |
3 4 |
from_typing_import_many.py:5:18: FA100 [*] Add `from __future__ import annotations` to simplify `typing.Optional`
|
4 | def main() -> None:
5 | a_list: List[Optional[str]] = []
@@ -19,3 +25,10 @@ from_typing_import_many.py:5:18: FA100 Add `from __future__ import annotations`
6 | a_list.append("hello")
7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {})
|
= help: Add `from __future__ import annotations`
Unsafe fix
1 |+from __future__ import annotations
1 2 | from typing import Dict, List, Optional, Set, Union, cast
2 3 |
3 4 |

View File

@@ -1,11 +1,17 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
snapshot_kind: text
---
import_typing.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
import_typing.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List`
|
4 | def main() -> None:
5 | a_list: typing.List[str] = []
| ^^^^^^^^^^^ FA100
6 | a_list.append("hello")
|
= help: Add `from __future__ import annotations`
Unsafe fix
1 |+from __future__ import annotations
1 2 | import typing
2 3 |
3 4 |

View File

@@ -1,11 +1,17 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
snapshot_kind: text
---
import_typing_as.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List`
import_typing_as.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List`
|
4 | def main() -> None:
5 | a_list: t.List[str] = []
| ^^^^^^ FA100
6 | a_list.append("hello")
|
= help: Add `from __future__ import annotations`
Unsafe fix
1 |+from __future__ import annotations
1 2 | import typing as t
2 3 |
3 4 |

View File

@@ -59,6 +59,7 @@ impl Violation for ExcInfoOutsideExceptHandler {
}
}
/// LOG014
pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall) {
let semantic = checker.semantic();

View File

@@ -47,22 +47,16 @@ fn check_msg(checker: &Checker, msg: &Expr) {
// Check for string concatenation and percent format.
Expr::BinOp(ast::ExprBinOp { op, .. }) => match op {
Operator::Add => {
if checker.is_rule_enabled(Rule::LoggingStringConcat) {
checker.report_diagnostic(LoggingStringConcat, msg.range());
}
checker.report_diagnostic_if_enabled(LoggingStringConcat, msg.range());
}
Operator::Mod => {
if checker.is_rule_enabled(Rule::LoggingPercentFormat) {
checker.report_diagnostic(LoggingPercentFormat, msg.range());
}
checker.report_diagnostic_if_enabled(LoggingPercentFormat, msg.range());
}
_ => {}
},
// Check for f-strings.
Expr::FString(_) => {
if checker.is_rule_enabled(Rule::LoggingFString) {
checker.report_diagnostic(LoggingFString, msg.range());
}
checker.report_diagnostic_if_enabled(LoggingFString, msg.range());
}
// Check for .format() calls.
Expr::Call(ast::ExprCall { func, .. }) => {
@@ -171,7 +165,7 @@ pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) {
_ => return,
};
// G001 - G004
// G001, G002, G003, G004
let msg_pos = usize::from(matches!(logging_call_type, LoggingCallType::LogCall));
if let Some(format_arg) = call.arguments.find_argument_value("msg", msg_pos) {
check_msg(checker, format_arg);
@@ -209,14 +203,10 @@ pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) {
if let LoggingCallType::LevelCall(logging_level) = logging_call_type {
match logging_level {
LoggingLevel::Error => {
if checker.is_rule_enabled(Rule::LoggingExcInfo) {
checker.report_diagnostic(LoggingExcInfo, range);
}
checker.report_diagnostic_if_enabled(LoggingExcInfo, range);
}
LoggingLevel::Exception => {
if checker.is_rule_enabled(Rule::LoggingRedundantExcInfo) {
checker.report_diagnostic(LoggingRedundantExcInfo, exc_info.range());
}
checker.report_diagnostic_if_enabled(LoggingRedundantExcInfo, exc_info.range());
}
_ => {}
}

View File

@@ -148,8 +148,6 @@ pub(crate) fn bad_version_info_comparison(checker: &Checker, test: &Expr, has_el
}
}
} else {
if checker.is_rule_enabled(Rule::BadVersionInfoComparison) {
checker.report_diagnostic(BadVersionInfoComparison, test.range());
}
checker.report_diagnostic_if_enabled(BadVersionInfoComparison, test.range());
}
}

View File

@@ -114,9 +114,7 @@ pub(crate) fn unrecognized_platform(checker: &Checker, test: &Expr) {
// "in" might also make sense but we don't currently have one.
if !matches!(op, CmpOp::Eq | CmpOp::NotEq) {
if checker.is_rule_enabled(Rule::UnrecognizedPlatformCheck) {
checker.report_diagnostic(UnrecognizedPlatformCheck, test.range());
}
checker.report_diagnostic_if_enabled(UnrecognizedPlatformCheck, test.range());
return;
}
@@ -134,8 +132,6 @@ pub(crate) fn unrecognized_platform(checker: &Checker, test: &Expr) {
}
}
} else {
if checker.is_rule_enabled(Rule::UnrecognizedPlatformCheck) {
checker.report_diagnostic(UnrecognizedPlatformCheck, test.range());
}
checker.report_diagnostic_if_enabled(UnrecognizedPlatformCheck, test.range());
}
}

View File

@@ -147,9 +147,7 @@ pub(crate) fn unrecognized_version_info(checker: &Checker, test: &Expr) {
if let Some(expected) = ExpectedComparator::try_from(left) {
version_check(checker, expected, test, *op, comparator);
} else {
if checker.is_rule_enabled(Rule::UnrecognizedVersionInfoCheck) {
checker.report_diagnostic(UnrecognizedVersionInfoCheck, test.range());
}
checker.report_diagnostic_if_enabled(UnrecognizedVersionInfoCheck, test.range());
}
}
@@ -163,33 +161,25 @@ fn version_check(
// Single digit comparison, e.g., `sys.version_info[0] == 2`.
if expected == ExpectedComparator::MajorDigit {
if !is_int_constant(comparator) {
if checker.is_rule_enabled(Rule::UnrecognizedVersionInfoCheck) {
checker.report_diagnostic(UnrecognizedVersionInfoCheck, test.range());
}
checker.report_diagnostic_if_enabled(UnrecognizedVersionInfoCheck, test.range());
}
return;
}
// Tuple comparison, e.g., `sys.version_info == (3, 4)`.
let Expr::Tuple(tuple) = comparator else {
if checker.is_rule_enabled(Rule::UnrecognizedVersionInfoCheck) {
checker.report_diagnostic(UnrecognizedVersionInfoCheck, test.range());
}
checker.report_diagnostic_if_enabled(UnrecognizedVersionInfoCheck, test.range());
return;
};
if !tuple.iter().all(is_int_constant) {
// All tuple elements must be integers, e.g., `sys.version_info == (3, 4)` instead of
// `sys.version_info == (3.0, 4)`.
if checker.is_rule_enabled(Rule::UnrecognizedVersionInfoCheck) {
checker.report_diagnostic(UnrecognizedVersionInfoCheck, test.range());
}
checker.report_diagnostic_if_enabled(UnrecognizedVersionInfoCheck, test.range());
} else if tuple.len() > 2 {
// Must compare against major and minor version only, e.g., `sys.version_info == (3, 4)`
// instead of `sys.version_info == (3, 4, 0)`.
if checker.is_rule_enabled(Rule::PatchVersionComparison) {
checker.report_diagnostic(PatchVersionComparison, test.range());
}
checker.report_diagnostic_if_enabled(PatchVersionComparison, test.range());
}
if checker.is_rule_enabled(Rule::WrongTupleLengthVersionComparison) {

View File

@@ -55,6 +55,7 @@ impl Violation for PytestFailWithoutMessage {
}
}
/// PT016
pub(crate) fn fail_call(checker: &Checker, call: &ast::ExprCall) {
if is_pytest_fail(&call.func, checker.semantic()) {
// Allow either `pytest.fail(reason="...")` (introduced in pytest 7.0) or

View File

@@ -225,6 +225,7 @@ fn check_useless_usefixtures(checker: &Checker, decorator: &Decorator, marker: &
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion(decorator.range())));
}
/// PT023, PT026
pub(crate) fn marks(checker: &Checker, decorators: &[Decorator]) {
let enforce_parentheses = checker.is_rule_enabled(Rule::PytestIncorrectMarkParenthesesStyle);
let enforce_useless_usefixtures =

View File

@@ -170,6 +170,7 @@ const fn is_non_trivial_with_body(body: &[Stmt]) -> bool {
}
}
/// PT010
pub(crate) fn raises_call(checker: &Checker, call: &ast::ExprCall) {
if is_pytest_raises(&call.func, checker.semantic()) {
if checker.is_rule_enabled(Rule::PytestRaisesWithoutException) {
@@ -205,6 +206,7 @@ pub(crate) fn raises_call(checker: &Checker, call: &ast::ExprCall) {
}
}
/// PT012
pub(crate) fn complex_raises(checker: &Checker, stmt: &Stmt, items: &[WithItem], body: &[Stmt]) {
let raises_called = items.iter().any(|item| match &item.context_expr {
Expr::Call(ast::ExprCall { func, .. }) => is_pytest_raises(func, checker.semantic()),

View File

@@ -12,6 +12,7 @@ mod tests {
use crate::assert_diagnostics;
use crate::registry::Rule;
use crate::settings;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
#[test_case(Path::new("full_name.py"))]
@@ -58,6 +59,7 @@ mod tests {
#[test_case(Rule::PyPath, Path::new("py_path_2.py"))]
#[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]
#[test_case(Rule::OsPathGetmtime, Path::new("PTH204.py"))]
#[test_case(Rule::OsPathGetctime, Path::new("PTH205.py"))]
@@ -76,4 +78,23 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]
fn preview_flake8_use_pathlib(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_use_pathlib").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -1,6 +1,11 @@
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_path_getsize_enabled;
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{Expr, ExprCall};
use ruff_text_size::Ranged;
/// ## What it does
/// Checks for uses of `os.path.getsize`.
@@ -32,6 +37,9 @@ use crate::Violation;
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
@@ -43,8 +51,77 @@ use crate::Violation;
pub(crate) struct OsPathGetsize;
impl Violation for OsPathGetsize {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.getsize` should be replaced by `Path.stat().st_size`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).stat().st_size`".to_string())
}
}
/// PTH202
pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall) {
if !matches!(
checker
.semantic()
.resolve_qualified_name(&call.func)
.as_ref()
.map(QualifiedName::segments),
Some(["os", "path", "getsize"])
) {
return;
}
if call.arguments.len() != 1 {
return;
}
let Some(arg) = call.arguments.find_argument_value("filename", 0) else {
return;
};
let arg_code = checker.locator().slice(arg.range());
let range = call.range();
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let mut diagnostic = checker.report_diagnostic(OsPathGetsize, range);
if is_fix_os_path_getsize_enabled(checker.settings()) {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let replacement = if is_path_call(checker, arg) {
format!("{arg_code}.stat().st_size")
} else {
format!("{binding}({arg_code}).stat().st_size")
};
Ok(
Fix::safe_edits(Edit::range_replacement(replacement, range), [import_edit])
.with_applicability(applicability),
)
});
}
}
fn is_path_call(checker: &Checker, expr: &Expr) -> bool {
expr.as_call_expr().is_some_and(|expr_call| {
checker
.semantic()
.resolve_qualified_name(&expr_call.func)
.is_some_and(|name| matches!(name.segments(), ["pathlib", "Path"]))
})
}

View File

@@ -5,7 +5,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::flake8_use_pathlib::rules::{
Glob, OsPathGetatime, OsPathGetctime, OsPathGetmtime, OsPathGetsize,
Glob, OsPathGetatime, OsPathGetctime, OsPathGetmtime,
};
use crate::rules::flake8_use_pathlib::violations::{
BuiltinOpen, Joiner, OsChmod, OsGetcwd, OsListdir, OsMakedirs, OsMkdir, OsPathAbspath,
@@ -194,8 +194,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
["os", "path", "samefile"] => checker.report_diagnostic_if_enabled(OsPathSamefile, range),
// PTH122
["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range),
// PTH202
["os", "path", "getsize"] => checker.report_diagnostic_if_enabled(OsPathGetsize, range),
// PTH203
["os", "path", "getatime"] => checker.report_diagnostic_if_enabled(OsPathGetatime, range),
// PTH204

View File

@@ -1,74 +1,357 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH202.py:6:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
6 | os.path.getsize("filename")
| ^^^^^^^^^^^^^^^ PTH202
7 | os.path.getsize(b"filename")
8 | os.path.getsize(Path("filename"))
|
PTH202.py:7:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
6 | os.path.getsize("filename")
7 | os.path.getsize(b"filename")
| ^^^^^^^^^^^^^^^ PTH202
8 | os.path.getsize(Path("filename"))
9 | os.path.getsize(__file__)
|
PTH202.py:8:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
6 | os.path.getsize("filename")
7 | os.path.getsize(b"filename")
8 | os.path.getsize(Path("filename"))
| ^^^^^^^^^^^^^^^ PTH202
9 | os.path.getsize(__file__)
|
PTH202.py:9:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
PTH202.py:10:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
7 | os.path.getsize(b"filename")
8 | os.path.getsize(Path("filename"))
9 | os.path.getsize(__file__)
| ^^^^^^^^^^^^^^^ PTH202
10 |
11 | getsize("filename")
10 | os.path.getsize("filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:11:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
9 | os.path.getsize(__file__)
10 |
11 | getsize("filename")
| ^^^^^^^ PTH202
12 | getsize(b"filename")
13 | getsize(Path("filename"))
10 | os.path.getsize("filename")
11 | os.path.getsize(b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
12 | os.path.getsize(Path("filename"))
13 | os.path.getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:12:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
11 | getsize("filename")
12 | getsize(b"filename")
| ^^^^^^^ PTH202
13 | getsize(Path("filename"))
14 | getsize(__file__)
10 | os.path.getsize("filename")
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
13 | os.path.getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:13:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
11 | getsize("filename")
12 | getsize(b"filename")
13 | getsize(Path("filename"))
| ^^^^^^^ PTH202
14 | getsize(__file__)
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
13 | os.path.getsize(__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
14 |
15 | os.path.getsize(filename)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:14:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
PTH202.py:15:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
12 | getsize(b"filename")
13 | getsize(Path("filename"))
14 | getsize(__file__)
| ^^^^^^^ PTH202
13 | os.path.getsize(__file__)
14 |
15 | os.path.getsize(filename)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
16 | os.path.getsize(filename1)
17 | os.path.getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:16:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
15 | os.path.getsize(filename)
16 | os.path.getsize(filename1)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
17 | os.path.getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:17:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
15 | os.path.getsize(filename)
16 | os.path.getsize(filename1)
17 | os.path.getsize(filename2)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
18 |
19 | os.path.getsize(filename="filename")
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:19:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
17 | os.path.getsize(filename2)
18 |
19 | os.path.getsize(filename="filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:20:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
19 | os.path.getsize(filename="filename")
20 | os.path.getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
21 | os.path.getsize(filename=Path("filename"))
22 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:21:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
19 | os.path.getsize(filename="filename")
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
22 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:22:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
22 | os.path.getsize(filename=__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
23 |
24 | getsize("filename")
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:24:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
22 | os.path.getsize(filename=__file__)
23 |
24 | getsize("filename")
| ^^^^^^^^^^^^^^^^^^^ PTH202
25 | getsize(b"filename")
26 | getsize(Path("filename"))
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:25:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
24 | getsize("filename")
25 | getsize(b"filename")
| ^^^^^^^^^^^^^^^^^^^^ PTH202
26 | getsize(Path("filename"))
27 | getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:26:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
24 | getsize("filename")
25 | getsize(b"filename")
26 | getsize(Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
27 | getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:27:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
25 | getsize(b"filename")
26 | getsize(Path("filename"))
27 | getsize(__file__)
| ^^^^^^^^^^^^^^^^^ PTH202
28 |
29 | getsize(filename="filename")
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:29:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
27 | getsize(__file__)
28 |
29 | getsize(filename="filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:30:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
29 | getsize(filename="filename")
30 | getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
31 | getsize(filename=Path("filename"))
32 | getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:31:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
29 | getsize(filename="filename")
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
32 | getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:32:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
32 | getsize(filename=__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
33 |
34 | getsize(filename)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:34:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
32 | getsize(filename=__file__)
33 |
34 | getsize(filename)
| ^^^^^^^^^^^^^^^^^ PTH202
35 | getsize(filename1)
36 | getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:35:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
34 | getsize(filename)
35 | getsize(filename1)
| ^^^^^^^^^^^^^^^^^^ PTH202
36 | getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:36:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
34 | getsize(filename)
35 | getsize(filename1)
36 | getsize(filename2)
| ^^^^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:39:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
39 | / os.path.getsize(
40 | | "filename", # comment
41 | | )
| |_^ PTH202
42 |
43 | os.path.getsize(
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:43:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
41 | )
42 |
43 | / os.path.getsize(
44 | | # comment
45 | | "filename"
46 | | ,
47 | | # comment
48 | | )
| |_^ PTH202
49 |
50 | os.path.getsize(
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:50:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
48 | )
49 |
50 | / os.path.getsize(
51 | | # comment
52 | | b"filename"
53 | | # comment
54 | | )
| |_^ PTH202
55 |
56 | os.path.getsize( # comment
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:56:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
54 | )
55 |
56 | / os.path.getsize( # comment
57 | | Path(__file__)
58 | | # comment
59 | | ) # comment
| |_^ PTH202
60 |
61 | getsize( # comment
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:61:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
59 | ) # comment
60 |
61 | / getsize( # comment
62 | | "filename")
| |_______________^ PTH202
63 |
64 | getsize( # comment
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:64:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
62 | "filename")
63 |
64 | / getsize( # comment
65 | | b"filename",
66 | | #comment
67 | | )
| |_^ PTH202
68 |
69 | os.path.getsize("file" + "name")
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:69:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
67 | )
68 |
69 | os.path.getsize("file" + "name")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
70 |
71 | getsize \
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:71:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
69 | os.path.getsize("file" + "name")
70 |
71 | / getsize \
72 | | \
73 | | \
74 | | ( # comment
75 | | "filename",
76 | | )
| |_____^ PTH202
77 |
78 | getsize(Path("filename").resolve())
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:78:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
76 | )
77 |
78 | getsize(Path("filename").resolve())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
79 |
80 | import pathlib
|
= help: Replace with `Path(...).stat().st_size`
PTH202.py:82:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
80 | import pathlib
81 |
82 | os.path.getsize(pathlib.Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`

View File

@@ -0,0 +1,31 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH202_2.py:3:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
1 | import os
2 |
3 | os.path.getsize(filename="filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
4 | os.path.getsize(filename=b"filename")
5 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
PTH202_2.py:4:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
3 | os.path.getsize(filename="filename")
4 | os.path.getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
5 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
PTH202_2.py:5:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
3 | os.path.getsize(filename="filename")
4 | os.path.getsize(filename=b"filename")
5 | os.path.getsize(filename=__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`

View File

@@ -0,0 +1,697 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH202.py:10:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
10 | os.path.getsize("filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
7 7 | filename2 = Path("filename")
8 8 |
9 9 |
10 |-os.path.getsize("filename")
10 |+Path("filename").stat().st_size
11 11 | os.path.getsize(b"filename")
12 12 | os.path.getsize(Path("filename"))
13 13 | os.path.getsize(__file__)
PTH202.py:11:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
10 | os.path.getsize("filename")
11 | os.path.getsize(b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
12 | os.path.getsize(Path("filename"))
13 | os.path.getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
8 8 |
9 9 |
10 10 | os.path.getsize("filename")
11 |-os.path.getsize(b"filename")
11 |+Path(b"filename").stat().st_size
12 12 | os.path.getsize(Path("filename"))
13 13 | os.path.getsize(__file__)
14 14 |
PTH202.py:12:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
10 | os.path.getsize("filename")
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
13 | os.path.getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
9 9 |
10 10 | os.path.getsize("filename")
11 11 | os.path.getsize(b"filename")
12 |-os.path.getsize(Path("filename"))
12 |+Path("filename").stat().st_size
13 13 | os.path.getsize(__file__)
14 14 |
15 15 | os.path.getsize(filename)
PTH202.py:13:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
11 | os.path.getsize(b"filename")
12 | os.path.getsize(Path("filename"))
13 | os.path.getsize(__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
14 |
15 | os.path.getsize(filename)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
10 10 | os.path.getsize("filename")
11 11 | os.path.getsize(b"filename")
12 12 | os.path.getsize(Path("filename"))
13 |-os.path.getsize(__file__)
13 |+Path(__file__).stat().st_size
14 14 |
15 15 | os.path.getsize(filename)
16 16 | os.path.getsize(filename1)
PTH202.py:15:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
13 | os.path.getsize(__file__)
14 |
15 | os.path.getsize(filename)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
16 | os.path.getsize(filename1)
17 | os.path.getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
12 12 | os.path.getsize(Path("filename"))
13 13 | os.path.getsize(__file__)
14 14 |
15 |-os.path.getsize(filename)
15 |+Path(filename).stat().st_size
16 16 | os.path.getsize(filename1)
17 17 | os.path.getsize(filename2)
18 18 |
PTH202.py:16:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
15 | os.path.getsize(filename)
16 | os.path.getsize(filename1)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
17 | os.path.getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
13 13 | os.path.getsize(__file__)
14 14 |
15 15 | os.path.getsize(filename)
16 |-os.path.getsize(filename1)
16 |+Path(filename1).stat().st_size
17 17 | os.path.getsize(filename2)
18 18 |
19 19 | os.path.getsize(filename="filename")
PTH202.py:17:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
15 | os.path.getsize(filename)
16 | os.path.getsize(filename1)
17 | os.path.getsize(filename2)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
18 |
19 | os.path.getsize(filename="filename")
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
14 14 |
15 15 | os.path.getsize(filename)
16 16 | os.path.getsize(filename1)
17 |-os.path.getsize(filename2)
17 |+Path(filename2).stat().st_size
18 18 |
19 19 | os.path.getsize(filename="filename")
20 20 | os.path.getsize(filename=b"filename")
PTH202.py:19:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
17 | os.path.getsize(filename2)
18 |
19 | os.path.getsize(filename="filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
16 16 | os.path.getsize(filename1)
17 17 | os.path.getsize(filename2)
18 18 |
19 |-os.path.getsize(filename="filename")
19 |+Path("filename").stat().st_size
20 20 | os.path.getsize(filename=b"filename")
21 21 | os.path.getsize(filename=Path("filename"))
22 22 | os.path.getsize(filename=__file__)
PTH202.py:20:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
19 | os.path.getsize(filename="filename")
20 | os.path.getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
21 | os.path.getsize(filename=Path("filename"))
22 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
17 17 | os.path.getsize(filename2)
18 18 |
19 19 | os.path.getsize(filename="filename")
20 |-os.path.getsize(filename=b"filename")
20 |+Path(b"filename").stat().st_size
21 21 | os.path.getsize(filename=Path("filename"))
22 22 | os.path.getsize(filename=__file__)
23 23 |
PTH202.py:21:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
19 | os.path.getsize(filename="filename")
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
22 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
18 18 |
19 19 | os.path.getsize(filename="filename")
20 20 | os.path.getsize(filename=b"filename")
21 |-os.path.getsize(filename=Path("filename"))
21 |+Path("filename").stat().st_size
22 22 | os.path.getsize(filename=__file__)
23 23 |
24 24 | getsize("filename")
PTH202.py:22:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
20 | os.path.getsize(filename=b"filename")
21 | os.path.getsize(filename=Path("filename"))
22 | os.path.getsize(filename=__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
23 |
24 | getsize("filename")
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
19 19 | os.path.getsize(filename="filename")
20 20 | os.path.getsize(filename=b"filename")
21 21 | os.path.getsize(filename=Path("filename"))
22 |-os.path.getsize(filename=__file__)
22 |+Path(__file__).stat().st_size
23 23 |
24 24 | getsize("filename")
25 25 | getsize(b"filename")
PTH202.py:24:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
22 | os.path.getsize(filename=__file__)
23 |
24 | getsize("filename")
| ^^^^^^^^^^^^^^^^^^^ PTH202
25 | getsize(b"filename")
26 | getsize(Path("filename"))
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
21 21 | os.path.getsize(filename=Path("filename"))
22 22 | os.path.getsize(filename=__file__)
23 23 |
24 |-getsize("filename")
24 |+Path("filename").stat().st_size
25 25 | getsize(b"filename")
26 26 | getsize(Path("filename"))
27 27 | getsize(__file__)
PTH202.py:25:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
24 | getsize("filename")
25 | getsize(b"filename")
| ^^^^^^^^^^^^^^^^^^^^ PTH202
26 | getsize(Path("filename"))
27 | getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
22 22 | os.path.getsize(filename=__file__)
23 23 |
24 24 | getsize("filename")
25 |-getsize(b"filename")
25 |+Path(b"filename").stat().st_size
26 26 | getsize(Path("filename"))
27 27 | getsize(__file__)
28 28 |
PTH202.py:26:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
24 | getsize("filename")
25 | getsize(b"filename")
26 | getsize(Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
27 | getsize(__file__)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
23 23 |
24 24 | getsize("filename")
25 25 | getsize(b"filename")
26 |-getsize(Path("filename"))
26 |+Path("filename").stat().st_size
27 27 | getsize(__file__)
28 28 |
29 29 | getsize(filename="filename")
PTH202.py:27:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
25 | getsize(b"filename")
26 | getsize(Path("filename"))
27 | getsize(__file__)
| ^^^^^^^^^^^^^^^^^ PTH202
28 |
29 | getsize(filename="filename")
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
24 24 | getsize("filename")
25 25 | getsize(b"filename")
26 26 | getsize(Path("filename"))
27 |-getsize(__file__)
27 |+Path(__file__).stat().st_size
28 28 |
29 29 | getsize(filename="filename")
30 30 | getsize(filename=b"filename")
PTH202.py:29:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
27 | getsize(__file__)
28 |
29 | getsize(filename="filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
26 26 | getsize(Path("filename"))
27 27 | getsize(__file__)
28 28 |
29 |-getsize(filename="filename")
29 |+Path("filename").stat().st_size
30 30 | getsize(filename=b"filename")
31 31 | getsize(filename=Path("filename"))
32 32 | getsize(filename=__file__)
PTH202.py:30:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
29 | getsize(filename="filename")
30 | getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
31 | getsize(filename=Path("filename"))
32 | getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
27 27 | getsize(__file__)
28 28 |
29 29 | getsize(filename="filename")
30 |-getsize(filename=b"filename")
30 |+Path(b"filename").stat().st_size
31 31 | getsize(filename=Path("filename"))
32 32 | getsize(filename=__file__)
33 33 |
PTH202.py:31:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
29 | getsize(filename="filename")
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
32 | getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
28 28 |
29 29 | getsize(filename="filename")
30 30 | getsize(filename=b"filename")
31 |-getsize(filename=Path("filename"))
31 |+Path("filename").stat().st_size
32 32 | getsize(filename=__file__)
33 33 |
34 34 | getsize(filename)
PTH202.py:32:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
30 | getsize(filename=b"filename")
31 | getsize(filename=Path("filename"))
32 | getsize(filename=__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
33 |
34 | getsize(filename)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
29 29 | getsize(filename="filename")
30 30 | getsize(filename=b"filename")
31 31 | getsize(filename=Path("filename"))
32 |-getsize(filename=__file__)
32 |+Path(__file__).stat().st_size
33 33 |
34 34 | getsize(filename)
35 35 | getsize(filename1)
PTH202.py:34:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
32 | getsize(filename=__file__)
33 |
34 | getsize(filename)
| ^^^^^^^^^^^^^^^^^ PTH202
35 | getsize(filename1)
36 | getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
31 31 | getsize(filename=Path("filename"))
32 32 | getsize(filename=__file__)
33 33 |
34 |-getsize(filename)
34 |+Path(filename).stat().st_size
35 35 | getsize(filename1)
36 36 | getsize(filename2)
37 37 |
PTH202.py:35:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
34 | getsize(filename)
35 | getsize(filename1)
| ^^^^^^^^^^^^^^^^^^ PTH202
36 | getsize(filename2)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
32 32 | getsize(filename=__file__)
33 33 |
34 34 | getsize(filename)
35 |-getsize(filename1)
35 |+Path(filename1).stat().st_size
36 36 | getsize(filename2)
37 37 |
38 38 |
PTH202.py:36:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
34 | getsize(filename)
35 | getsize(filename1)
36 | getsize(filename2)
| ^^^^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
33 33 |
34 34 | getsize(filename)
35 35 | getsize(filename1)
36 |-getsize(filename2)
36 |+Path(filename2).stat().st_size
37 37 |
38 38 |
39 39 | os.path.getsize(
PTH202.py:39:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
39 | / os.path.getsize(
40 | | "filename", # comment
41 | | )
| |_^ PTH202
42 |
43 | os.path.getsize(
|
= help: Replace with `Path(...).stat().st_size`
Unsafe fix
36 36 | getsize(filename2)
37 37 |
38 38 |
39 |-os.path.getsize(
40 |- "filename", # comment
41 |-)
39 |+Path("filename").stat().st_size
42 40 |
43 41 | os.path.getsize(
44 42 | # comment
PTH202.py:43:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
41 | )
42 |
43 | / os.path.getsize(
44 | | # comment
45 | | "filename"
46 | | ,
47 | | # comment
48 | | )
| |_^ PTH202
49 |
50 | os.path.getsize(
|
= help: Replace with `Path(...).stat().st_size`
Unsafe fix
40 40 | "filename", # comment
41 41 | )
42 42 |
43 |-os.path.getsize(
44 |- # comment
45 |- "filename"
46 |- ,
47 |- # comment
48 |-)
43 |+Path("filename").stat().st_size
49 44 |
50 45 | os.path.getsize(
51 46 | # comment
PTH202.py:50:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
48 | )
49 |
50 | / os.path.getsize(
51 | | # comment
52 | | b"filename"
53 | | # comment
54 | | )
| |_^ PTH202
55 |
56 | os.path.getsize( # comment
|
= help: Replace with `Path(...).stat().st_size`
Unsafe fix
47 47 | # comment
48 48 | )
49 49 |
50 |-os.path.getsize(
51 |- # comment
52 |- b"filename"
53 |- # comment
54 |-)
50 |+Path(b"filename").stat().st_size
55 51 |
56 52 | os.path.getsize( # comment
57 53 | Path(__file__)
PTH202.py:56:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
54 | )
55 |
56 | / os.path.getsize( # comment
57 | | Path(__file__)
58 | | # comment
59 | | ) # comment
| |_^ PTH202
60 |
61 | getsize( # comment
|
= help: Replace with `Path(...).stat().st_size`
Unsafe fix
53 53 | # comment
54 54 | )
55 55 |
56 |-os.path.getsize( # comment
57 |- Path(__file__)
58 |- # comment
59 |-) # comment
56 |+Path(__file__).stat().st_size # comment
60 57 |
61 58 | getsize( # comment
62 59 | "filename")
PTH202.py:61:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
59 | ) # comment
60 |
61 | / getsize( # comment
62 | | "filename")
| |_______________^ PTH202
63 |
64 | getsize( # comment
|
= help: Replace with `Path(...).stat().st_size`
Unsafe fix
58 58 | # comment
59 59 | ) # comment
60 60 |
61 |-getsize( # comment
62 |- "filename")
61 |+Path("filename").stat().st_size
63 62 |
64 63 | getsize( # comment
65 64 | b"filename",
PTH202.py:64:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
62 | "filename")
63 |
64 | / getsize( # comment
65 | | b"filename",
66 | | #comment
67 | | )
| |_^ PTH202
68 |
69 | os.path.getsize("file" + "name")
|
= help: Replace with `Path(...).stat().st_size`
Unsafe fix
61 61 | getsize( # comment
62 62 | "filename")
63 63 |
64 |-getsize( # comment
65 |- b"filename",
66 |- #comment
67 |-)
64 |+Path(b"filename").stat().st_size
68 65 |
69 66 | os.path.getsize("file" + "name")
70 67 |
PTH202.py:69:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
67 | )
68 |
69 | os.path.getsize("file" + "name")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
70 |
71 | getsize \
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
66 66 | #comment
67 67 | )
68 68 |
69 |-os.path.getsize("file" + "name")
69 |+Path("file" + "name").stat().st_size
70 70 |
71 71 | getsize \
72 72 | \
PTH202.py:71:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
69 | os.path.getsize("file" + "name")
70 |
71 | / getsize \
72 | | \
73 | | \
74 | | ( # comment
75 | | "filename",
76 | | )
| |_____^ PTH202
77 |
78 | getsize(Path("filename").resolve())
|
= help: Replace with `Path(...).stat().st_size`
Unsafe fix
68 68 |
69 69 | os.path.getsize("file" + "name")
70 70 |
71 |-getsize \
72 |-\
73 |-\
74 |- ( # comment
75 |- "filename",
76 |- )
71 |+Path("filename").stat().st_size
77 72 |
78 73 | getsize(Path("filename").resolve())
79 74 |
PTH202.py:78:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
76 | )
77 |
78 | getsize(Path("filename").resolve())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
79 |
80 | import pathlib
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
75 75 | "filename",
76 76 | )
77 77 |
78 |-getsize(Path("filename").resolve())
78 |+Path(Path("filename").resolve()).stat().st_size
79 79 |
80 80 | import pathlib
81 81 |
PTH202.py:82:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
80 | import pathlib
81 |
82 | os.path.getsize(pathlib.Path("filename"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
79 79 |
80 80 | import pathlib
81 81 |
82 |-os.path.getsize(pathlib.Path("filename"))
82 |+pathlib.Path("filename").stat().st_size

View File

@@ -0,0 +1,58 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH202_2.py:3:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
1 | import os
2 |
3 | os.path.getsize(filename="filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
4 | os.path.getsize(filename=b"filename")
5 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
1 1 | import os
2 |+import pathlib
2 3 |
3 |-os.path.getsize(filename="filename")
4 |+pathlib.Path("filename").stat().st_size
4 5 | os.path.getsize(filename=b"filename")
5 6 | os.path.getsize(filename=__file__)
PTH202_2.py:4:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
3 | os.path.getsize(filename="filename")
4 | os.path.getsize(filename=b"filename")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
5 | os.path.getsize(filename=__file__)
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
1 1 | import os
2 |+import pathlib
2 3 |
3 4 | os.path.getsize(filename="filename")
4 |-os.path.getsize(filename=b"filename")
5 |+pathlib.Path(b"filename").stat().st_size
5 6 | os.path.getsize(filename=__file__)
PTH202_2.py:5:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size`
|
3 | os.path.getsize(filename="filename")
4 | os.path.getsize(filename=b"filename")
5 | os.path.getsize(filename=__file__)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PTH202
|
= help: Replace with `Path(...).stat().st_size`
Safe fix
1 1 | import os
2 |+import pathlib
2 3 |
3 4 | os.path.getsize(filename="filename")
4 5 | os.path.getsize(filename=b"filename")
5 |-os.path.getsize(filename=__file__)
6 |+pathlib.Path(__file__).stat().st_size

View File

@@ -154,6 +154,7 @@ fn get_complexity_number(stmts: &[Stmt]) -> usize {
complexity
}
/// C901
pub(crate) fn function_is_too_complex(
checker: &Checker,
stmt: &Stmt,

View File

@@ -181,15 +181,19 @@ pub(crate) fn call(checker: &Checker, func: &Expr) {
let range = func.range();
match attr.as_str() {
// PD003
"isnull" if checker.is_rule_enabled(Rule::PandasUseOfDotIsNull) => {
checker.report_diagnostic(PandasUseOfDotIsNull, range);
}
// PD004
"notnull" if checker.is_rule_enabled(Rule::PandasUseOfDotNotNull) => {
checker.report_diagnostic(PandasUseOfDotNotNull, range);
}
// PD010
"pivot" | "unstack" if checker.is_rule_enabled(Rule::PandasUseOfDotPivotOrUnstack) => {
checker.report_diagnostic(PandasUseOfDotPivotOrUnstack, range);
}
// PD013
"stack" if checker.is_rule_enabled(Rule::PandasUseOfDotStack) => {
checker.report_diagnostic(PandasUseOfDotStack, range);
}

View File

@@ -163,12 +163,15 @@ pub(crate) fn subscript(checker: &Checker, value: &Expr, expr: &Expr) {
let range = expr.range();
match attr.as_str() {
// PD007
"ix" if checker.is_rule_enabled(Rule::PandasUseOfDotIx) => {
checker.report_diagnostic(PandasUseOfDotIx, range)
}
// PD008
"at" if checker.is_rule_enabled(Rule::PandasUseOfDotAt) => {
checker.report_diagnostic(PandasUseOfDotAt, range)
}
// PD009
"iat" if checker.is_rule_enabled(Rule::PandasUseOfDotIat) => {
checker.report_diagnostic(PandasUseOfDotIat, range)
}

View File

@@ -4,7 +4,7 @@ use ruff_python_ast::{
};
use ruff_python_semantic::{Binding, analyze::typing::is_dict};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker;
use crate::preview::is_fix_manual_dict_comprehension_enabled;
@@ -139,28 +139,11 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, for_stmt: &ast::StmtF
};
// If any references to a target variable are after the loop,
// then removing the loop would cause a NameError
let any_references_after_for_loop = |target: &Expr| {
let target_binding = checker
.semantic()
.bindings
.iter()
.find(|binding| target.range() == binding.range);
debug_assert!(
target_binding.is_some(),
"for-loop target binding must exist"
);
let Some(target_binding) = target_binding else {
// All uses of this function will early-return if this returns true, so this must early-return the rule
return true;
};
target_binding
.references()
.map(|reference| checker.semantic().reference(reference))
.any(|other_reference| other_reference.start() > for_stmt.end())
};
// then removing the loop would cause a NameError. Make sure none
// of the variables are used outside the for loop.
if has_post_loop_references(checker, target, for_stmt.end()) {
return;
}
match target {
Expr::Tuple(tuple) => {
@@ -176,10 +159,6 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, for_stmt: &ast::StmtF
{
return;
}
// Make sure none of the variables are used outside the for loop
if tuple.iter().any(any_references_after_for_loop) {
return;
}
}
Expr::Name(_) => {
if ComparableExpr::from(key) != ComparableExpr::from(target) {
@@ -188,12 +167,6 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, for_stmt: &ast::StmtF
if ComparableExpr::from(value) != ComparableExpr::from(target) {
return;
}
// We know that `target` contains an ExprName, but closures can't take `&impl Ranged`,
// so we pass `target` itself instead of the inner ExprName
if any_references_after_for_loop(target) {
return;
}
}
_ => return,
}
@@ -380,9 +353,18 @@ fn convert_to_dict_comprehension(
} else {
"for"
};
// Handles the case where `key` has a trailing comma, e.g, `dict[x,] = y`
let key_range = if let Expr::Tuple(ast::ExprTuple { elts, .. }) = key {
let [expr] = elts.as_slice() else {
return None;
};
expr.range()
} else {
key.range()
};
let elt_str = format!(
"{}: {}",
locator.slice(key.range()),
locator.slice(key_range),
locator.slice(value.range())
);
@@ -473,3 +455,25 @@ enum DictComprehensionType {
Update,
Comprehension,
}
fn has_post_loop_references(checker: &Checker, expr: &Expr, loop_end: TextSize) -> bool {
any_over_expr(expr, &|expr| match expr {
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts
.iter()
.any(|expr| has_post_loop_references(checker, expr, loop_end)),
Expr::Name(name) => {
let target_binding = checker
.semantic()
.bindings
.iter()
.find(|binding| name.range() == binding.range)
.expect("for-loop target binding must exist");
target_binding
.references()
.map(|reference| checker.semantic().reference(reference))
.any(|other_reference| other_reference.start() > loop_end)
}
_ => false,
})
}

View File

@@ -146,5 +146,16 @@ PERF403.py:166:13: PERF403 Use a dictionary comprehension instead of a for-loop
165 | if lambda: 0:
166 | dst[k] = v
| ^^^^^^^^^^ PERF403
167 |
168 | # https://github.com/astral-sh/ruff/issues/18859
|
= help: Replace for loop with dict comprehension
PERF403.py:172:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
170 | v = {}
171 | for o,(x,)in():
172 | v[x,]=o
| ^^^^^^^ PERF403
|
= help: Replace for loop with dict comprehension

View File

@@ -340,6 +340,8 @@ PERF403.py:166:13: PERF403 [*] Use `dict.update` instead of a for-loop
165 | if lambda: 0:
166 | dst[k] = v
| ^^^^^^^^^^ PERF403
167 |
168 | # https://github.com/astral-sh/ruff/issues/18859
|
= help: Replace for loop with `dict.update`
@@ -351,3 +353,24 @@ PERF403.py:166:13: PERF403 [*] Use `dict.update` instead of a for-loop
165 |- if lambda: 0:
166 |- dst[k] = v
164 |+ dst.update({k: v for k, v in src if (lambda: 0)})
167 165 |
168 166 | # https://github.com/astral-sh/ruff/issues/18859
169 167 | def foo():
PERF403.py:172:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop
|
170 | v = {}
171 | for o,(x,)in():
172 | v[x,]=o
| ^^^^^^^ PERF403
|
= help: Replace for loop with dict comprehension
Unsafe fix
167 167 |
168 168 | # https://github.com/astral-sh/ruff/issues/18859
169 169 | def foo():
170 |- v = {}
171 |- for o,(x,)in():
172 |- v[x,]=o
170 |+ v = {x: o for o,(x,) in ()}

View File

@@ -4,7 +4,6 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::docstrings::Docstring;
use crate::registry::Rule;
/// ## What it does
/// Checks for empty docstrings.
@@ -44,9 +43,6 @@ pub(crate) fn not_empty(checker: &Checker, docstring: &Docstring) -> bool {
if !docstring.body().trim().is_empty() {
return true;
}
if checker.is_rule_enabled(Rule::EmptyDocstring) {
checker.report_diagnostic(EmptyDocstring, docstring.range());
}
checker.report_diagnostic_if_enabled(EmptyDocstring, docstring.range());
false
}

View File

@@ -8,7 +8,6 @@ use ruff_text_size::TextRange;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
/// ## What it does
/// Checks for undocumented public module definitions.
@@ -551,36 +550,29 @@ pub(crate) fn not_missing(
if checker.source_type.is_ipynb() {
return true;
}
if checker.is_rule_enabled(Rule::UndocumentedPublicModule) {
checker.report_diagnostic(UndocumentedPublicModule, TextRange::default());
}
checker.report_diagnostic_if_enabled(UndocumentedPublicModule, TextRange::default());
false
}
Definition::Module(Module {
kind: ModuleKind::Package,
..
}) => {
if checker.is_rule_enabled(Rule::UndocumentedPublicPackage) {
checker.report_diagnostic(UndocumentedPublicPackage, TextRange::default());
}
checker.report_diagnostic_if_enabled(UndocumentedPublicPackage, TextRange::default());
false
}
Definition::Member(Member {
kind: MemberKind::Class(class),
..
}) => {
if checker.is_rule_enabled(Rule::UndocumentedPublicClass) {
checker.report_diagnostic(UndocumentedPublicClass, class.identifier());
}
checker.report_diagnostic_if_enabled(UndocumentedPublicClass, class.identifier());
false
}
Definition::Member(Member {
kind: MemberKind::NestedClass(function),
..
}) => {
if checker.is_rule_enabled(Rule::UndocumentedPublicNestedClass) {
checker.report_diagnostic(UndocumentedPublicNestedClass, function.identifier());
}
checker
.report_diagnostic_if_enabled(UndocumentedPublicNestedClass, function.identifier());
false
}
Definition::Member(Member {
@@ -590,9 +582,10 @@ pub(crate) fn not_missing(
if is_overload(&function.decorator_list, checker.semantic()) {
true
} else {
if checker.is_rule_enabled(Rule::UndocumentedPublicFunction) {
checker.report_diagnostic(UndocumentedPublicFunction, function.identifier());
}
checker.report_diagnostic_if_enabled(
UndocumentedPublicFunction,
function.identifier(),
);
false
}
}
@@ -605,24 +598,19 @@ pub(crate) fn not_missing(
{
true
} else if is_init(&function.name) {
if checker.is_rule_enabled(Rule::UndocumentedPublicInit) {
checker.report_diagnostic(UndocumentedPublicInit, function.identifier());
}
checker.report_diagnostic_if_enabled(UndocumentedPublicInit, function.identifier());
true
} else if is_new(&function.name) || is_call(&function.name) {
if checker.is_rule_enabled(Rule::UndocumentedPublicMethod) {
checker.report_diagnostic(UndocumentedPublicMethod, function.identifier());
}
checker
.report_diagnostic_if_enabled(UndocumentedPublicMethod, function.identifier());
true
} else if is_magic(&function.name) {
if checker.is_rule_enabled(Rule::UndocumentedMagicMethod) {
checker.report_diagnostic(UndocumentedMagicMethod, function.identifier());
}
checker
.report_diagnostic_if_enabled(UndocumentedMagicMethod, function.identifier());
true
} else {
if checker.is_rule_enabled(Rule::UndocumentedPublicMethod) {
checker.report_diagnostic(UndocumentedPublicMethod, function.identifier());
}
checker
.report_diagnostic_if_enabled(UndocumentedPublicMethod, function.identifier());
true
}
}

View File

@@ -1434,14 +1434,12 @@ fn blanks_and_section_underline(
}
if following_lines.peek().is_none() {
if checker.is_rule_enabled(Rule::EmptyDocstringSection) {
checker.report_diagnostic(
EmptyDocstringSection {
name: context.section_name().to_string(),
},
context.section_name_range(),
);
}
checker.report_diagnostic_if_enabled(
EmptyDocstringSection {
name: context.section_name().to_string(),
},
context.section_name_range(),
);
} else if checker.is_rule_enabled(Rule::BlankLinesBetweenHeaderAndContent) {
// If the section is followed by exactly one line, and then a
// reStructuredText directive, the blank lines should be preserved, as in:
@@ -1495,14 +1493,12 @@ fn blanks_and_section_underline(
}
}
} else {
if checker.is_rule_enabled(Rule::EmptyDocstringSection) {
checker.report_diagnostic(
EmptyDocstringSection {
name: context.section_name().to_string(),
},
context.section_name_range(),
);
}
checker.report_diagnostic_if_enabled(
EmptyDocstringSection {
name: context.section_name().to_string(),
},
context.section_name_range(),
);
}
} else {
if style.is_numpy() && checker.is_rule_enabled(Rule::MissingDashedUnderlineAfterSection)
@@ -1618,14 +1614,12 @@ fn blanks_and_section_underline(
context.summary_range().end(),
)));
}
if checker.is_rule_enabled(Rule::EmptyDocstringSection) {
checker.report_diagnostic(
EmptyDocstringSection {
name: context.section_name().to_string(),
},
context.section_name_range(),
);
}
checker.report_diagnostic_if_enabled(
EmptyDocstringSection {
name: context.section_name().to_string(),
},
context.section_name_range(),
);
}
}

View File

@@ -30,6 +30,7 @@ impl Violation for FutureFeatureNotDefined {
}
}
/// F407
pub(crate) fn future_feature_not_defined(checker: &Checker, alias: &Alias) {
if is_feature_name(&alias.name) {
return;

View File

@@ -1,7 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::{BindingKind, Scope, ScopeId};
use ruff_source_file::SourceRow;
use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for import bindings that are shadowed by loop variables.
@@ -44,6 +47,48 @@ impl Violation for ImportShadowedByLoopVar {
}
}
/// F402
pub(crate) fn import_shadowed_by_loop_var(checker: &Checker, scope_id: ScopeId, scope: &Scope) {
for (name, binding_id) in scope.bindings() {
for shadow in checker.semantic().shadowed_bindings(scope_id, binding_id) {
// If the shadowing binding isn't a loop variable, abort.
let binding = &checker.semantic().bindings[shadow.binding_id()];
if !binding.kind.is_loop_var() {
continue;
}
// If the shadowed binding isn't an import, abort.
let shadowed = &checker.semantic().bindings[shadow.shadowed_id()];
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) {
continue;
}
// If the bindings are in different forks, abort.
if shadowed.source.is_none_or(|left| {
binding
.source
.is_none_or(|right| !checker.semantic().same_branch(left, right))
}) {
continue;
}
checker.report_diagnostic(
ImportShadowedByLoopVar {
name: name.to_string(),
row: checker.compute_source_row(shadowed.start()),
},
binding.range(),
);
}
}
}
/// ## What it does
/// Checks for the use of wildcard imports.
///

View File

@@ -1,7 +1,14 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{BindingKind, Imported, Scope, ScopeId};
use ruff_source_file::SourceRow;
use ruff_text_size::Ranged;
use crate::{FixAvailability, Violation};
use crate::checkers::ast::Checker;
use crate::fix::edits;
use crate::{Fix, FixAvailability, Violation};
use rustc_hash::FxHashMap;
/// ## What it does
/// Checks for variable definitions that redefine (or "shadow") unused
@@ -43,3 +50,154 @@ impl Violation for RedefinedWhileUnused {
Some(format!("Remove definition: `{name}`"))
}
}
/// F811
pub(crate) fn redefined_while_unused(checker: &Checker, scope_id: ScopeId, scope: &Scope) {
// Index the redefined bindings by statement.
let mut redefinitions = FxHashMap::default();
for (name, binding_id) in scope.bindings() {
for shadow in checker.semantic().shadowed_bindings(scope_id, binding_id) {
// If the shadowing binding is a loop variable, abort, to avoid overlap
// with F402.
let binding = &checker.semantic().bindings[shadow.binding_id()];
if binding.kind.is_loop_var() {
continue;
}
// If the shadowed binding is used, abort.
let shadowed = &checker.semantic().bindings[shadow.shadowed_id()];
if shadowed.is_used() {
continue;
}
// If the shadowing binding isn't considered a "redefinition" of the
// shadowed binding, abort.
if !binding.redefines(shadowed) {
continue;
}
if shadow.same_scope() {
// If the symbol is a dummy variable, abort, unless the shadowed
// binding is an import.
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) && checker.settings().dummy_variable_rgx.is_match(name)
{
continue;
}
let Some(node_id) = shadowed.source else {
continue;
};
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition() {
if checker
.semantic()
.statement(node_id)
.as_function_def_stmt()
.is_some_and(|function| {
visibility::is_overload(&function.decorator_list, checker.semantic())
})
{
continue;
}
}
} else {
// Only enforce cross-scope shadowing for imports.
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) {
continue;
}
}
// If the bindings are in different forks, abort.
if shadowed.source.is_none_or(|left| {
binding
.source
.is_none_or(|right| !checker.semantic().same_branch(left, right))
}) {
continue;
}
redefinitions
.entry(binding.source)
.or_insert_with(Vec::new)
.push((shadowed, binding));
}
}
// Create a fix for each source statement.
let mut fixes = FxHashMap::default();
for (source, entries) in &redefinitions {
let Some(source) = source else {
continue;
};
let member_names = entries
.iter()
.filter_map(|(shadowed, binding)| {
if let Some(shadowed_import) = shadowed.as_any_import() {
if let Some(import) = binding.as_any_import() {
if shadowed_import.qualified_name() == import.qualified_name() {
return Some(import.member_name());
}
}
}
None
})
.collect::<Vec<_>>();
if !member_names.is_empty() {
let statement = checker.semantic().statement(*source);
let parent = checker.semantic().parent_statement(*source);
let Ok(edit) = edits::remove_unused_imports(
member_names.iter().map(std::convert::AsRef::as_ref),
statement,
parent,
checker.locator(),
checker.stylist(),
checker.indexer(),
) else {
continue;
};
fixes.insert(
*source,
Fix::safe_edit(edit).isolate(Checker::isolation(
checker.semantic().parent_statement_id(*source),
)),
);
}
}
// Create diagnostics for each statement.
for (source, entries) in &redefinitions {
for (shadowed, binding) in entries {
let mut diagnostic = checker.report_diagnostic(
RedefinedWhileUnused {
name: binding.name(checker.source()).to_string(),
row: checker.compute_source_row(shadowed.start()),
},
binding.range(),
);
if let Some(range) = binding.parent_range(checker.semantic()) {
diagnostic.set_parent(range.start());
}
if let Some(fix) = source.as_ref().and_then(|source| fixes.get(source)) {
diagnostic.set_fix(fix.clone());
}
}
}
}

View File

@@ -1,5 +1,7 @@
use std::string::ToString;
use ruff_diagnostics::Applicability;
use ruff_python_ast::helpers::contains_effect;
use rustc_hash::FxHashSet;
use ruff_macros::{ViolationMetadata, derive_message_formats};
@@ -138,6 +140,16 @@ impl Violation for PercentFormatExpectedSequence {
/// "Hello, %(name)s" % {"name": "World"}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe for mapping key
/// containing function calls with potential side effects,
/// because removing such arguments could change the behavior of the code.
///
/// For example, the fix would be marked as unsafe in the following case:
/// ```python
/// "Hello, %(name)s" % {"greeting": print(1), "name": "World"}
/// ```
///
/// ## References
/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting)
#[derive(ViolationMetadata)]
@@ -379,6 +391,16 @@ impl Violation for StringDotFormatInvalidFormat {
/// "Hello, {name}".format(name="World")
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe if the unused keyword argument
/// contains a function call with potential side effects,
/// because removing such arguments could change the behavior of the code.
///
/// For example, the fix would be marked as unsafe in the following case:
/// ```python
/// "Hello, {name}".format(greeting=print(1), name="World")
/// ```
///
/// ## References
/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format)
#[derive(ViolationMetadata)]
@@ -420,6 +442,16 @@ impl Violation for StringDotFormatExtraNamedArguments {
/// "Hello, {0}".format("world")
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe if the unused positional argument
/// contains a function call with potential side effects,
/// because removing such arguments could change the behavior of the code.
///
/// For example, the fix would be marked as unsafe in the following case:
/// ```python
/// "Hello, {0}".format("world", print(1))
/// ```
///
/// ## References
/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format)
#[derive(ViolationMetadata)]
@@ -603,15 +635,24 @@ pub(crate) fn percent_format_extra_named_arguments(
PercentFormatExtraNamedArguments { missing: names },
location,
);
let indexes: Vec<usize> = missing.iter().map(|(index, _)| *index).collect();
diagnostic.try_set_fix(|| {
let indexes: Vec<usize> = missing.iter().map(|(index, _)| *index).collect();
let edit = remove_unused_format_arguments_from_dict(
&indexes,
dict,
checker.locator(),
checker.stylist(),
)?;
Ok(Fix::safe_edit(edit))
Ok(Fix::applicable_edit(
edit,
// Mark fix as unsafe if `dict` contains a call with side effect
if contains_effect(right, |id| checker.semantic().has_builtin_binding(id)) {
Applicability::Unsafe
} else {
Applicability::Safe
},
))
});
}
@@ -734,16 +775,19 @@ pub(crate) fn string_dot_format_extra_named_arguments(
return;
}
let keywords = keywords
let keyword_names = keywords
.iter()
.filter_map(|Keyword { arg, .. }| arg.as_ref());
.filter_map(|Keyword { arg, value, .. }| Some((arg.as_ref()?, value)));
let missing: Vec<(usize, &Name)> = keywords
let mut side_effects = false;
let missing: Vec<(usize, &Name)> = keyword_names
.enumerate()
.filter_map(|(index, keyword)| {
.filter_map(|(index, (keyword, value))| {
if summary.keywords.contains(keyword.id()) {
None
} else {
side_effects |=
contains_effect(value, |id| checker.semantic().has_builtin_binding(id));
Some((index, &keyword.id))
}
})
@@ -758,7 +802,7 @@ pub(crate) fn string_dot_format_extra_named_arguments(
StringDotFormatExtraNamedArguments { missing: names },
call.range(),
);
let indexes: Vec<usize> = missing.iter().map(|(index, _)| *index).collect();
let indexes: Vec<usize> = missing.into_iter().map(|(index, _)| index).collect();
diagnostic.try_set_fix(|| {
let edit = remove_unused_keyword_arguments_from_format_call(
&indexes,
@@ -766,7 +810,16 @@ pub(crate) fn string_dot_format_extra_named_arguments(
checker.locator(),
checker.stylist(),
)?;
Ok(Fix::safe_edit(edit))
Ok(Fix::applicable_edit(
edit,
// Mark fix as unsafe if the `format` call contains an argument with side effect
if side_effects {
Applicability::Unsafe
} else {
Applicability::Safe
},
))
});
}
@@ -802,13 +855,17 @@ pub(crate) fn string_dot_format_extra_positional_arguments(
true
}
let mut side_effects = false;
let missing: Vec<usize> = args
.iter()
.enumerate()
.filter(|(i, arg)| {
!(arg.is_starred_expr() || summary.autos.contains(i) || summary.indices.contains(i))
})
.map(|(i, _)| i)
.map(|(i, arg)| {
side_effects |= contains_effect(arg, |id| checker.semantic().has_builtin_binding(id));
i
})
.collect();
if missing.is_empty() {
@@ -833,7 +890,15 @@ pub(crate) fn string_dot_format_extra_positional_arguments(
checker.locator(),
checker.stylist(),
)?;
Ok(Fix::safe_edit(edit))
Ok(Fix::applicable_edit(
edit,
// Mark fix as unsafe if the `format` call contains an argument with side effect
if side_effects {
Applicability::Unsafe
} else {
Applicability::Safe
},
))
});
}
}

View File

@@ -276,6 +276,7 @@ fn find_dunder_all_exprs<'a>(semantic: &'a SemanticModel) -> Vec<&'a ast::Expr>
.collect()
}
/// F401
/// For some unused binding in an import statement...
///
/// __init__.py ∧ 1stpty → safe, if one __all__, add to __all__

View File

@@ -89,6 +89,8 @@ F504.py:14:1: F504 [*] `%`-format string has unused named argument(s): test1, te
15 | | 'test1': '',
'test2': '',
16 | | }
| |_^ F504
17 |
18 | # https://github.com/astral-sh/ruff/issues/18806
|
= help: Remove extra named arguments: test1, test2
@@ -99,3 +101,20 @@ F504.py:14:1: F504 [*] `%`-format string has unused named argument(s): test1, te
14 14 | "" % {
15 |- 'test1': '',
16 |- 'test2': '',
15 |+
17 16 | }
18 17 |
19 18 | # https://github.com/astral-sh/ruff/issues/18806
F504.py:20:1: F504 [*] `%`-format string has unused named argument(s): greeting
|
19 | # https://github.com/astral-sh/ruff/issues/18806
20 | "Hello, %(name)s" % {"greeting": print(1), "name": "World"}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F504
|
= help: Remove extra named arguments: greeting
Unsafe fix
17 17 | }
18 18 |
19 19 | # https://github.com/astral-sh/ruff/issues/18806

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
snapshot_kind: text
---
F522.py:1:1: F522 [*] `.format` call has unused named argument(s): bar
|
@@ -55,6 +54,7 @@ F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham
4 |+"{bar:{spam}}".format(bar=2, spam=3, ) # F522
5 5 | (''
6 6 | .format(x=2)) # F522
7 7 |
F522.py:5:2: F522 [*] `.format` call has unused named argument(s): x
|
@@ -64,6 +64,8 @@ F522.py:5:2: F522 [*] `.format` call has unused named argument(s): x
| __^
6 | | .format(x=2)) # F522
| |_____________^ F522
7 |
8 | # https://github.com/astral-sh/ruff/issues/18806
|
= help: Remove extra named arguments: x
@@ -73,3 +75,43 @@ F522.py:5:2: F522 [*] `.format` call has unused named argument(s): x
5 5 | (''
6 |- .format(x=2)) # F522
6 |+ .format()) # F522
7 7 |
8 8 | # https://github.com/astral-sh/ruff/issues/18806
9 9 | # The fix here is unsafe because the unused argument has side effect
F522.py:10:1: F522 [*] `.format` call has unused named argument(s): greeting
|
8 | # https://github.com/astral-sh/ruff/issues/18806
9 | # The fix here is unsafe because the unused argument has side effect
10 | "Hello, {name}".format(greeting=print(1), name="World")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F522
11 |
12 | # The fix here is safe because the unused argument has no side effect,
|
= help: Remove extra named arguments: greeting
Unsafe fix
7 7 |
8 8 | # https://github.com/astral-sh/ruff/issues/18806
9 9 | # The fix here is unsafe because the unused argument has side effect
10 |-"Hello, {name}".format(greeting=print(1), name="World")
10 |+"Hello, {name}".format(name="World")
11 11 |
12 12 | # The fix here is safe because the unused argument has no side effect,
13 13 | # even though the used argument has a side effect
F522.py:14:1: F522 [*] `.format` call has unused named argument(s): greeting
|
12 | # The fix here is safe because the unused argument has no side effect,
13 | # even though the used argument has a side effect
14 | "Hello, {name}".format(greeting="Pikachu", name=print(1))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F522
|
= help: Remove extra named arguments: greeting
Safe fix
11 11 |
12 12 | # The fix here is safe because the unused argument has no side effect,
13 13 | # even though the used argument has a side effect
14 |-"Hello, {name}".format(greeting="Pikachu", name=print(1))
14 |+"Hello, {name}".format(name=print(1))

View File

@@ -286,6 +286,8 @@ F523.py:36:1: F523 [*] `.format` call has unused arguments at position(s): 0
36 |-"Hello".format("world")
36 |+"Hello"
37 37 | "Hello".format("world", key="value")
38 38 |
39 39 | # https://github.com/astral-sh/ruff/issues/18806
F523.py:37:1: F523 [*] `.format` call has unused arguments at position(s): 0
|
@@ -293,6 +295,8 @@ F523.py:37:1: F523 [*] `.format` call has unused arguments at position(s): 0
36 | "Hello".format("world")
37 | "Hello".format("world", key="value")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523
38 |
39 | # https://github.com/astral-sh/ruff/issues/18806
|
= help: Remove extra positional arguments at position(s): 0
@@ -302,3 +306,43 @@ F523.py:37:1: F523 [*] `.format` call has unused arguments at position(s): 0
36 36 | "Hello".format("world")
37 |-"Hello".format("world", key="value")
37 |+"Hello".format(key="value")
38 38 |
39 39 | # https://github.com/astral-sh/ruff/issues/18806
40 40 | # The fix here is unsafe because the unused argument has side effect
F523.py:41:1: F523 [*] `.format` call has unused arguments at position(s): 1
|
39 | # https://github.com/astral-sh/ruff/issues/18806
40 | # The fix here is unsafe because the unused argument has side effect
41 | "Hello, {0}".format("world", print(1))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523
42 |
43 | # The fix here is safe because the unused argument has no side effect,
|
= help: Remove extra positional arguments at position(s): 1
Unsafe fix
38 38 |
39 39 | # https://github.com/astral-sh/ruff/issues/18806
40 40 | # The fix here is unsafe because the unused argument has side effect
41 |-"Hello, {0}".format("world", print(1))
41 |+"Hello, {0}".format("world", )
42 42 |
43 43 | # The fix here is safe because the unused argument has no side effect,
44 44 | # even though the used argument has a side effect
F523.py:45:1: F523 [*] `.format` call has unused arguments at position(s): 1
|
43 | # The fix here is safe because the unused argument has no side effect,
44 | # even though the used argument has a side effect
45 | "Hello, {0}".format(print(1), "Pikachu")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523
|
= help: Remove extra positional arguments at position(s): 1
Safe fix
42 42 |
43 43 | # The fix here is safe because the unused argument has no side effect,
44 44 | # even though the used argument has a side effect
45 |-"Hello, {0}".format(print(1), "Pikachu")
45 |+"Hello, {0}".format(print(1), )

View File

@@ -10,6 +10,7 @@ mod tests {
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_diagnostics, settings};
@@ -29,4 +30,22 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::InvalidMockAccess, Path::new("PGH005_0.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("pygrep_hooks").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -5,6 +5,7 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_invalid_async_mock_access_check_enabled;
#[derive(Debug, PartialEq, Eq)]
enum Reason {
@@ -51,7 +52,7 @@ impl Violation for InvalidMockAccess {
/// PGH005
pub(crate) fn uncalled_mock_method(checker: &Checker, expr: &Expr) {
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = expr {
if matches!(
let is_uncalled_mock_method = matches!(
attr.as_str(),
"assert_any_call"
| "assert_called"
@@ -60,7 +61,20 @@ pub(crate) fn uncalled_mock_method(checker: &Checker, expr: &Expr) {
| "assert_called_with"
| "assert_has_calls"
| "assert_not_called"
) {
);
let is_uncalled_async_mock_method =
is_invalid_async_mock_access_check_enabled(checker.settings())
&& matches!(
attr.as_str(),
"assert_awaited"
| "assert_awaited_once"
| "assert_awaited_with"
| "assert_awaited_once_with"
| "assert_any_await"
| "assert_has_awaits"
| "assert_not_awaited"
);
if is_uncalled_mock_method || is_uncalled_async_mock_method {
checker.report_diagnostic(
InvalidMockAccess {
reason: Reason::UncalledMethod(attr.to_string()),
@@ -81,7 +95,7 @@ pub(crate) fn non_existent_mock_method(checker: &Checker, test: &Expr) {
},
_ => return,
};
if matches!(
let is_missing_mock_method = matches!(
attr.as_str(),
"any_call"
| "called_once"
@@ -89,7 +103,20 @@ pub(crate) fn non_existent_mock_method(checker: &Checker, test: &Expr) {
| "called_with"
| "has_calls"
| "not_called"
) {
);
let is_missing_async_mock_method =
is_invalid_async_mock_access_check_enabled(checker.settings())
&& matches!(
attr.as_str(),
"awaited"
| "awaited_once"
| "awaited_with"
| "awaited_once_with"
| "any_await"
| "has_awaits"
| "not_awaited"
);
if is_missing_mock_method || is_missing_async_mock_method {
checker.report_diagnostic(
InvalidMockAccess {
reason: Reason::NonExistentMethod(attr.to_string()),

View File

@@ -1,90 +1,91 @@
---
source: crates/ruff_linter/src/rules/pygrep_hooks/mod.rs
---
PGH005_0.py:2:8: PGH005 Non-existent mock method: `not_called`
|
1 | # Errors
2 | assert my_mock.not_called()
| ^^^^^^^^^^^^^^^^^^^^ PGH005
3 | assert my_mock.called_once_with()
4 | assert my_mock.not_called
|
PGH005_0.py:3:8: PGH005 Non-existent mock method: `called_once_with`
|
1 | # Errors
2 | assert my_mock.not_called()
3 | assert my_mock.called_once_with()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
4 | assert my_mock.not_called
5 | assert my_mock.called_once_with
|
PGH005_0.py:4:8: PGH005 Non-existent mock method: `not_called`
|
2 | assert my_mock.not_called()
3 | assert my_mock.called_once_with()
4 | assert my_mock.not_called
| ^^^^^^^^^^^^^^^^^^ PGH005
5 | assert my_mock.called_once_with
6 | my_mock.assert_not_called
2 | # ============
3 | # Errors
4 | assert my_mock.not_called()
| ^^^^^^^^^^^^^^^^^^^^ PGH005
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
|
PGH005_0.py:5:8: PGH005 Non-existent mock method: `called_once_with`
|
3 | assert my_mock.called_once_with()
4 | assert my_mock.not_called
5 | assert my_mock.called_once_with
3 | # Errors
4 | assert my_mock.not_called()
5 | assert my_mock.called_once_with()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
|
PGH005_0.py:6:8: PGH005 Non-existent mock method: `not_called`
|
4 | assert my_mock.not_called()
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
| ^^^^^^^^^^^^^^^^^^ PGH005
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
|
PGH005_0.py:7:8: PGH005 Non-existent mock method: `called_once_with`
|
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
6 | my_mock.assert_not_called
7 | my_mock.assert_called
8 | my_mock.assert_not_called
9 | my_mock.assert_called
|
PGH005_0.py:6:1: PGH005 Mock method should be called: `assert_not_called`
|
4 | assert my_mock.not_called
5 | assert my_mock.called_once_with
6 | my_mock.assert_not_called
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
7 | my_mock.assert_called
8 | my_mock.assert_called_once_with
|
PGH005_0.py:7:1: PGH005 Mock method should be called: `assert_called`
|
5 | assert my_mock.called_once_with
6 | my_mock.assert_not_called
7 | my_mock.assert_called
| ^^^^^^^^^^^^^^^^^^^^^ PGH005
8 | my_mock.assert_called_once_with
9 | my_mock.assert_called_once_with
|
PGH005_0.py:8:1: PGH005 Mock method should be called: `assert_called_once_with`
PGH005_0.py:8:1: PGH005 Mock method should be called: `assert_not_called`
|
6 | my_mock.assert_not_called
7 | my_mock.assert_called
8 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
9 | my_mock.assert_called_once_with
10 | MyMock.assert_called_once_with
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
|
PGH005_0.py:9:1: PGH005 Mock method should be called: `assert_called_once_with`
PGH005_0.py:9:1: PGH005 Mock method should be called: `assert_called`
|
7 | my_mock.assert_called
8 | my_mock.assert_called_once_with
9 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
10 | MyMock.assert_called_once_with
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
9 | my_mock.assert_called
| ^^^^^^^^^^^^^^^^^^^^^ PGH005
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
|
PGH005_0.py:10:1: PGH005 Mock method should be called: `assert_called_once_with`
|
8 | my_mock.assert_called_once_with
9 | my_mock.assert_called_once_with
10 | MyMock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
11 |
12 | # OK
8 | my_mock.assert_not_called
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
11 | my_mock.assert_called_once_with
12 | MyMock.assert_called_once_with
|
PGH005_0.py:11:1: PGH005 Mock method should be called: `assert_called_once_with`
|
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
12 | MyMock.assert_called_once_with
|
PGH005_0.py:12:1: PGH005 Mock method should be called: `assert_called_once_with`
|
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
12 | MyMock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
13 |
14 | # OK
|

View File

@@ -0,0 +1,190 @@
---
source: crates/ruff_linter/src/rules/pygrep_hooks/mod.rs
---
PGH005_0.py:4:8: PGH005 Non-existent mock method: `not_called`
|
2 | # ============
3 | # Errors
4 | assert my_mock.not_called()
| ^^^^^^^^^^^^^^^^^^^^ PGH005
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
|
PGH005_0.py:5:8: PGH005 Non-existent mock method: `called_once_with`
|
3 | # Errors
4 | assert my_mock.not_called()
5 | assert my_mock.called_once_with()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
|
PGH005_0.py:6:8: PGH005 Non-existent mock method: `not_called`
|
4 | assert my_mock.not_called()
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
| ^^^^^^^^^^^^^^^^^^ PGH005
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
|
PGH005_0.py:7:8: PGH005 Non-existent mock method: `called_once_with`
|
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
8 | my_mock.assert_not_called
9 | my_mock.assert_called
|
PGH005_0.py:8:1: PGH005 Mock method should be called: `assert_not_called`
|
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
|
PGH005_0.py:9:1: PGH005 Mock method should be called: `assert_called`
|
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
9 | my_mock.assert_called
| ^^^^^^^^^^^^^^^^^^^^^ PGH005
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
|
PGH005_0.py:10:1: PGH005 Mock method should be called: `assert_called_once_with`
|
8 | my_mock.assert_not_called
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
11 | my_mock.assert_called_once_with
12 | MyMock.assert_called_once_with
|
PGH005_0.py:11:1: PGH005 Mock method should be called: `assert_called_once_with`
|
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
12 | MyMock.assert_called_once_with
|
PGH005_0.py:12:1: PGH005 Mock method should be called: `assert_called_once_with`
|
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
12 | MyMock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
13 |
14 | # OK
|
PGH005_0.py:26:8: PGH005 Non-existent mock method: `not_awaited`
|
24 | # =================
25 | # Errors
26 | assert my_mock.not_awaited()
| ^^^^^^^^^^^^^^^^^^^^^ PGH005
27 | assert my_mock.awaited_once_with()
28 | assert my_mock.not_awaited
|
PGH005_0.py:27:8: PGH005 Non-existent mock method: `awaited_once_with`
|
25 | # Errors
26 | assert my_mock.not_awaited()
27 | assert my_mock.awaited_once_with()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
28 | assert my_mock.not_awaited
29 | assert my_mock.awaited_once_with
|
PGH005_0.py:28:8: PGH005 Non-existent mock method: `not_awaited`
|
26 | assert my_mock.not_awaited()
27 | assert my_mock.awaited_once_with()
28 | assert my_mock.not_awaited
| ^^^^^^^^^^^^^^^^^^^ PGH005
29 | assert my_mock.awaited_once_with
30 | my_mock.assert_not_awaited
|
PGH005_0.py:29:8: PGH005 Non-existent mock method: `awaited_once_with`
|
27 | assert my_mock.awaited_once_with()
28 | assert my_mock.not_awaited
29 | assert my_mock.awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
30 | my_mock.assert_not_awaited
31 | my_mock.assert_awaited
|
PGH005_0.py:30:1: PGH005 Mock method should be called: `assert_not_awaited`
|
28 | assert my_mock.not_awaited
29 | assert my_mock.awaited_once_with
30 | my_mock.assert_not_awaited
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
31 | my_mock.assert_awaited
32 | my_mock.assert_awaited_once_with
|
PGH005_0.py:31:1: PGH005 Mock method should be called: `assert_awaited`
|
29 | assert my_mock.awaited_once_with
30 | my_mock.assert_not_awaited
31 | my_mock.assert_awaited
| ^^^^^^^^^^^^^^^^^^^^^^ PGH005
32 | my_mock.assert_awaited_once_with
33 | my_mock.assert_awaited_once_with
|
PGH005_0.py:32:1: PGH005 Mock method should be called: `assert_awaited_once_with`
|
30 | my_mock.assert_not_awaited
31 | my_mock.assert_awaited
32 | my_mock.assert_awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
33 | my_mock.assert_awaited_once_with
34 | MyMock.assert_awaited_once_with
|
PGH005_0.py:33:1: PGH005 Mock method should be called: `assert_awaited_once_with`
|
31 | my_mock.assert_awaited
32 | my_mock.assert_awaited_once_with
33 | my_mock.assert_awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
34 | MyMock.assert_awaited_once_with
35 | assert my_mock.awaited
|
PGH005_0.py:34:1: PGH005 Mock method should be called: `assert_awaited_once_with`
|
32 | my_mock.assert_awaited_once_with
33 | my_mock.assert_awaited_once_with
34 | MyMock.assert_awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
35 | assert my_mock.awaited
|
PGH005_0.py:35:8: PGH005 Non-existent mock method: `awaited`
|
33 | my_mock.assert_awaited_once_with
34 | MyMock.assert_awaited_once_with
35 | assert my_mock.awaited
| ^^^^^^^^^^^^^^^ PGH005
36 |
37 | # OK
|

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