Compare commits

...

129 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
Dhruv Manilawala
e474f36473 [ty] Avoid duplicate diagnostic in unpacking (#18897)
## Summary

This PR fixes astral-sh/ty#185 by avoiding to infer the value expression
for an unpacking.

This is done simply by only inferring the value expression in a
non-unpacking branch for assignment statement, for statement, with
statement and comprehensions.

This is a simpler alternative to
https://github.com/astral-sh/ruff/pull/18890 which I only realized in
hindsight! Ideally, the solution would to consider the "unpack" as it's
own region and do all of the inference of every expressions involved in
an unpacking inside the unpack query and then merge the results in the
outer query. This would require access to the `Unpack` ingredient which
is stored on the `Definition`. And, this would require create the said
`Definition`s for all attributes and subscript expressions. It does
simplify the target inference logic by streamlining it into a single
`infer_target` method instead of the `infer_target`/`infer_target_impl`
split.

Additionally, #18890 also solves a couple of TODOs around raising errors
around attribute / subscript assignment.

## Test Plan

Update the existing test, go through a couple of ecosystem diagnostic.
2025-06-24 07:49:44 +05:30
Igor Drokin
da16e00751 [pyupgrade] Extend version detection to include sys.version_info.major (UP036) (#18633)
## Summary

Resolves #18165 

Added pattern `["sys", "version_info", "major"]` to the existing matches
for `sys.version_info` to ensure consistent handling of both the base
object and its major version attribute.

## Test Plan
`cargo nextest run` and `cargo insta test`

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-06-23 20:01:55 +00:00
Илья Любавский
885dc9091f [ruff] Frozen Dataclass default should be valid (RUF009) (#18735)
<!--
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 #17424
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

<!-- How was it tested? -->
2025-06-23 19:10:12 +00:00
Andrew Gallant
d01e0faee3 [ty] Include imported sub-modules as attributes on modules for completions (#18898)
This also adds a new `ModuleName::relative_to` public API to help with
this.

Kudos to @AlexWaygood for the meat of this patch!

Ref https://github.com/astral-sh/ruff/pull/18830#discussion_r2161770991
2025-06-23 12:48:16 -04:00
Suneet Tipirneni
ef8281b695 [ty] add support for mapped union and intersection subscript loads (#18846)
## Summary

Note this modifies the diagnostics a bit. Previously performing
subscript access on something like `NotSubscriptable1 |
NotSubscriptable2` would report the full type as not being
subscriptable:

```
[non-subscriptable] "Cannot subscript object of type `NotSubscriptable1 | NotSubscriptable2` with no `__getitem__` method"
```

Now each erroneous constituent has a separate error:

```
[non-subscriptable] "Cannot subscript object of type `NotSubscriptable2` with no `__getitem__` method"
[non-subscriptable] "Cannot subscript object of type `NotSubscriptable1` with no `__getitem__` method"
```

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

## Test Plan

 mdtest

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-06-23 16:38:01 +00:00
Andrew Gallant
a77db3da3f [ty] Add completions for from module import <CURSOR> (#18830)
There were two main challenges in this PR.

The first was mostly just figuring out how to get the symbols
corresponding to `module`. It turns out that we do this in a couple
of places in ty already, but through different means. In one approach,
we use [`exported_names`]. In another approach, we get a `Type`
corresponding to the module. We take the latter approach here, which is
consistent with how we do completions elsewhere. (I looked into
factoring this logic out into its own function, but it ended up being
pretty constrained. e.g., There's only one other place where we want to
go from `ast::StmtImportFrom` to a module `Type`, and that code also
wants the module name.)

The second challenge was recognizing the `from module import <CURSOR>`
pattern in the code. I initially started with some fixed token patterns
to get a proof of concept working. But I ended up switching to mini
state machine over tokens. I looked at the parser for `StmtImportFrom`
to determine what kinds of tokens we can expect.

[`exported_names`]:
23a3b6ef23/crates/ty_python_semantic/src/semantic_index/re_exports.rs (L47)
2025-06-23 10:43:25 -04:00
Victor Hugo Gomes
9e9c4fe17b [flake8-simplify] Fix SIM911 autofix creating a syntax error (#18793)
<!--
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
The fix would create a syntax error if there wasn't a space between the
`in` keyword and the following expression.
For example:
```python
for country, stars in(zip)(flag_stars.keys(), flag_stars.values()):...
```

I also noticed that the tests for `SIM911` were note being run, so I
fixed that.

Fixes #18776

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

## Test Plan

Add regression test
<!-- How was it tested? -->
2025-06-23 16:24:47 +02:00
Victor Hugo Gomes
f4c6ff3f68 [pylint] Fix PLC2801 autofix creating a syntax error (#18857)
<!--
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
This PR fixes `PLC2801` autofix creating a syntax error due to lack of
padding if it is directly after a keyword.

Fixes https://github.com/astral-sh/ruff/issues/18813
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
Add regression test
<!-- How was it tested? -->
2025-06-23 10:15:53 -04:00
Vasco Schiavo
ca8ed35275 [flake8-simplify] Preserve original behavior for except () and bare except (SIM105) (#18213)
The PR addresses issue #18209.
2025-06-23 10:01:57 -04:00
GiGaGon
315fb0f3da [flake8-logging] Add fix safety section to LOG002 (#18840)
## Summary

Part of #15584

This adds a `Fix safety` section to [invalid-get-logger-argument
(LOG002)](https://docs.astral.sh/ruff/rules/invalid-get-logger-argument/#invalid-get-logger-argument-log002).

The fix/lint was introduced in #7399
No reasoning is given on the unsafety in the PR/code
Unsafe fix demonstration:
[playground](https://play.ruff.rs/e8008cbf-2ef5-4d38-8255-324f90e624cb)
```py
import logging
logger = logging.getLogger(__file__)
```

---------

Co-authored-by: Dylan <dylwil3@gmail.com>
2025-06-23 13:23:01 +00:00
GiGaGon
bbc26b2f11 [pyupgrade] Add fix safety section to UP004 (#18853)
<!--
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? -->

Part of #15584

This adds a `Fix safety` section to [useless-object-inheritance
(UP004)](https://docs.astral.sh/ruff/rules/useless-object-inheritance/#useless-object-inheritance-up004)

I could not track down the original PR as this rule is so old it has
gone through several large ruff refactors.
No reasoning is given on the unsafety in the PR/code.
The unsafety is determined here:

f24e650dfd/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs (L76-L80)

Unsafe fix demonstration:
[playground](https://play.ruff.rs/12b24eb4-d7a5-4ae0-93bb-492d64967ae3)
```py
class A(  # will be deleted
    object
):
    ...
```

## Test Plan

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

N/A, no tests/functionality affected
2025-06-23 08:22:36 -05:00
GiGaGon
ec07a0f885 [flake8-use-pathlib] Add fix safety section to PTH201 (#18837)
<!--
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? -->

Part of #15584

This adds a `Fix safety` section to [path-constructor-current-directory
(PTH201)](https://docs.astral.sh/ruff/rules/path-constructor-current-directory/#path-constructor-current-directory-pth201)

I could not track down the original PR as this rule is so old it has
gone through several large ruff refactors.
The unsafety is determined here:

d9266284df/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs (L55-L59)
Unsafe code example:
[playground](https://play.ruff.rs/76da532a-c7ad-4ef9-bba3-4626296e5317)
```py
from pathlib import Path
Path(#
    "."#
)
```

## Test Plan

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

N/A, no tests/functionality affected
2025-06-23 08:22:00 -05:00
GiGaGon
861dff1dd8 [refurb] Add fix safety section to FURB122 (#18842)
## Summary

Part of #15584

This adds a `Fix safety` section to [for-loop-writes
(FURB122)](https://docs.astral.sh/ruff/rules/for-loop-writes/#for-loop-writes-furb122).

The fix/lint was introduced in #10630
No reasoning is given on the unsafety in the PR/code.
The unsafety is determined here:

ea812d0813/crates/ruff_linter/src/rules/refurb/rules/for_loop_writes.rs (L200-L204)
Unsafe fix demonstration:
[playground](https://play.ruff.rs/06592f33-10b9-4a77-b31e-0d3a98f402f4)
```py
with open("issue.txt", "w") as f:
    for i in range(10):
        # will be deleted
        f.write(str(i))
```

---------

Co-authored-by: Dylan <dylwil3@gmail.com>
2025-06-23 13:21:37 +00:00
GiGaGon
8be205df99 [pyupgrade] Add fix safety section to UP010 (#18838)
<!--
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? -->

Part of #15584

This adds a `Fix safety` section to [unnecessary-future-import
(UP010)](https://docs.astral.sh/ruff/rules/unnecessary-future-import/#unnecessary-future-import-up010)

The unsafety is determined here:

d9266284df/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs (L128-L132)

Unsafe code example:
[playground](https://play.ruff.rs/c07d8c41-9ab8-4b86-805b-8cf482d450d9)
```py
from __future__ import (print_function,# ...
__annotations__)  # ...
```

Edit: It looks like there was already a PR for this, #17490, but I
missed it since they said `UP029` instead of `UP010` :/

## Test Plan

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

N/A, no tests/functionality affected
2025-06-23 08:20:55 -05:00
GiGaGon
34fd44eb55 [flake8-logging] Add fix safety section to LOG001 (#18841)
Part of #15584

This adds a `Fix safety` section to [direct-logger-instantiation
(LOG001)](https://docs.astral.sh/ruff/rules/direct-logger-instantiation/#direct-logger-instantiation-log001).

The fix/lint was introduced in #7397
No reasoning is given on the unsafety in the PR/code
Unsafe fix demonstration:
[playground](https://play.ruff.rs/72e7277e-a9db-4cd9-9afb-2c56ef2db361)
```py
import logging
logger = logging.Logger(__name__)
```

## Test Plan

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

N/A, no tests/functionality affected

---------

Co-authored-by: Dylan <dylwil3@gmail.com>
2025-06-23 13:19:23 +00:00
Micha Reiser
b64307f65e fix casing of analyze.direction variant names (#18892)
## Summary

Fixes `analyze.direction` to use kebab-case for the variant names. 

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

## Test Plan

Created a `ruff.toml` and tested that both `dependents` and `Dependents`
were accepted
2025-06-23 14:30:30 +02:00
Victor Hugo Gomes
291413b126 [perflint] Fix PERF101 autofix creating a syntax error and mark autofix as unsafe if there are comments in the list call expr (#18803)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-23 11:51:46 +00:00
Dan Parizher
7ec7853cec [flake8-pytest-style] PT001/PT023 fix makes syntax error on parenthesized decorator (#18782)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-23 13:46:15 +02:00
David Peter
907c291877 [ty] Update mypy_primer, add two new projects (#18891)
## Summary

Pull in latest changes to mypy_primer:
01a7ca325f..e5f5544796
2025-06-23 13:08:11 +02:00
David Peter
21303d1a02 [ty] Minor change to builtins.md test (#18889)
## Summary

As far as I can tell, the two existing tests did the exact same thing.
Remove the redundant test, and add tests for all combinations of
declared/not-declared and local/"public" use of the name.

Proposing this as a separate PR before the behavior might change via
https://github.com/astral-sh/ruff/pull/18750
2025-06-23 12:32:50 +02:00
Victor Hugo Gomes
528ae8083b Remove redundant settings field from Checker (#18845)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-23 11:06:44 +02:00
Victor Hugo Gomes
0ce022e64e [refurb] Fix FURB163 autofix creating a syntax error for yield expressions (#18756) 2025-06-23 10:13:03 +02:00
Victor Hugo Gomes
659ecba477 [pylint] Supress PLE2510/2512/2513/2514/2515 autofix if the text contains an odd number of backslashes (#18856)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-23 10:11:51 +02:00
renovate[bot]
96660d93ca Update docker/setup-buildx-action action to v3.11.1 (#18881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 08:06:43 +02:00
renovate[bot]
c111517f1b Update rui314/setup-mold digest to 85c79d0 (#18874)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 08:06:32 +02:00
renovate[bot]
9f29551fb2 Update PyO3/maturin-action action to v1.49.2 (#18875)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 08:06:18 +02:00
renovate[bot]
cc659c6988 Update Rust crate libc to v0.2.174 (#18876)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 08:06:03 +02:00
renovate[bot]
c8e03a0449 Update Rust crate mimalloc to v0.1.47 (#18877)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 08:05:40 +02:00
renovate[bot]
a5494839b1 Update astral-sh/setup-uv action to v6.3.0 (#18879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 08:03:06 +02:00
renovate[bot]
835b37818c Update cargo-bins/cargo-binstall action to v1.14.1 (#18880)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 08:02:25 +02:00
renovate[bot]
f88fbc3952 Update Rust crate syn to v2.0.104 (#18878)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 08:02:04 +02:00
Charlie Marsh
9570d39f9b Remove extra dot in rule documentation (#18871) 2025-06-23 00:33:21 +00:00
Victor Hugo Gomes
06a78d0bd0 [pylint] Fix PLC1802 autofix creating a syntax error and mark autofix as unsafe if there's comments in the len call (#18836)
<!--
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
I've also found another bug while fixing this, where the diagnostic
would not trigger if the `len` call argument variable was shadowed. This
fixed a few false negatives in the test cases.
Example:
```python
fruits = []
fruits = []
if len(fruits):  # comment
    ...
```

Fixes #18811
Fixes #18812
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
Add regression test
<!-- How was it tested? -->

---------

Co-authored-by: Charlie Marsh <crmarsh416@gmail.com>
2025-06-23 00:32:57 +00:00
Charlie Marsh
cfec89e8c3 Clarify PEP 8 relationship to whitespace-around-operator rules (#18870)
## Summary

See: https://github.com/astral-sh/ruff/issues/18868.
2025-06-22 20:30:34 -04:00
Victor Hugo Gomes
9089493263 [refurb] Mark FURB129 autofix as unsafe if there's comments in the readlines call (#18858) 2025-06-22 08:51:37 +01:00
Carl Meyer
089f5152f6 [ty] Fix mixed tuple subtyping (#18852)
## Summary

The code in the `Variable` branch of
`VariableLengthTupleSpec::has_relation_to` made the incorrect assumption
that if you zip two possibly-different-length iterators together and
iterate over the resulting zip iterator, the original two iterators will
only have their common elements consumed. But in fact, the zip iterator
detects that it is done when it receives a `None` from one iterator and
`Some()` element from the other iterator, which means that it consumes
one additional element from the longer iterator. This meant that we
failed to detect mismatched types on this extra consumed element,
because we never compared it to the variable type of the other tuple.

Use `zip_longest` from itertools as an alternative, which allows us to
combine all the handling into just two `zip_longest`, one for prefixes
and one for suffixes.

Marking this PR internal since it fixes a bug in a commit that wasn't
released yet.

## Test Plan

Added mdtests that failed before this fix and pass after it.
2025-06-21 13:09:23 -07:00
Alex Waygood
f24e650dfd [ty] Support --python=<symlink to executable> (#18827)
## Summary

Fixes https://github.com/astral-sh/ty/issues/640. If a user passes
`--python=<some-virtual-environment>/bin/python`, we must avoid
canonicalizing the path until we've traversed upwards to find the
`sys.prefix` directory (`<some-virtual-environment>`). On Unix systems,
`<sys.prefix>/bin/python` is often a symlink to a system interpreter; if
we resolve the symlink too easily then we'll add the system
interpreter's `site-packages` directory as a search path rather than the
virtual environment's directory.

## Test Plan

I added an integration test to
`crates/ty/tests/cli/python_environment.rs` which fails on `main`. I
also manually tested locally that running `cargo run -p ty check foo.py
--python=.venv/bin/python -vv` now prints this log to the terminal

```
2025-06-20 18:35:24.57702 DEBUG Resolved site-packages directories for this virtual environment are: SitePackagesPaths({"/Users/alexw/dev/ruff/.venv/lib/python3.13/site-packages"})
```

Whereas it previously resolved `site-packages` to my system
intallation's `site-packages` directory
2025-06-21 20:28:47 +01:00
Victor Hugo Gomes
f32ae94bc3 [pylint] Mark PLE0241 autofix as unsafe if there's comments in the base classes (#18832)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-21 17:20:39 +00:00
chiri
da6cbeee60 [flake8-raise] Make fix unsafe if it deletes comments (RSE102) (#18788) 2025-06-21 19:09:40 +02:00
Micha Reiser
cccbd0286e [ty] Add Tanjun benchmark (#18850) 2025-06-21 18:29:02 +02:00
Micha Reiser
a6ad8fb342 [ty] Add multithreaded benchmark (#18822) 2025-06-21 17:44:30 +02:00
Douglas Creager
ea812d0813 [ty] Homogeneous and mixed tuples (#18600)
We already had support for homogeneous tuples (`tuple[int, ...]`). This
PR extends this to also support mixed tuples (`tuple[str, str,
*tuple[int, ...], str str]`).

A mixed tuple consists of a fixed-length (possibly empty) prefix and
suffix, and a variable-length portion in the middle. Every element of
the variable-length portion must be of the same type. A homogeneous
tuple is then just a mixed tuple with an empty prefix and suffix.

The new data representation uses different Rust types for a fixed-length
(aka heterogeneous) tuple. Another option would have been to use the
`VariableLengthTuple` representation for all tuples, and to wrap the
"variable + suffix" portion in an `Option`. I don't think that would
simplify the method implementations much, though, since we would still
have a 2×2 case analysis for most of them.

One wrinkle is that the definition of the `tuple` class in the typeshed
has a single typevar, and canonically represents a homogeneous tuple.
When getting the class of a tuple instance, that means that we have to
summarize our detailed mixed tuple type information into its
"homogeneous supertype". (We were already doing this for heterogeneous
types.)

A similar thing happens when concatenating two mixed tuples: the
variable-length portion and suffix of the LHS, and the prefix and
variable-length portion of the RHS, all get unioned into the
variable-length portion of the result. The LHS prefix and RHS suffix
carry through unchanged.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-06-20 18:23:54 -04:00
Victor Hugo Gomes
d9266284df Handle parenthesized arguments in remove_argument (#18805)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-06-20 21:24:41 +00:00
Dylan
2d224e6096 Unify helpers modules (#18835)
A little bit of cleanup for consistency's sake: we move all the helpers
modules to a consistent location, and update the import paths when
needed. In the case of `refurb` there were two helpers modules, so we
just merged them.

Happy to revert the last commit if people are okay with `super::super` I
just thought it looked a little silly.
2025-06-20 16:03:01 -05:00
GiGaGon
ef785d2e74 Normalize some docs sections (#18831) 2025-06-20 21:56:11 +01:00
Robsdedude
e36611c4d8 [flake8_pyi] Fix PYI041's fix causing TypeError with None | None | ... (#18637)
<!--
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
Fix `PYI041`'s fix turning `None | int | None | float` into `None | None
| float`, which raises a `TypeError` when executed.

The fix consists of making sure that the merged super-type is inserted
where the first type that is merged was before.

## Test Plan
Tests have been expanded with examples from the issue.

## Related Issue
Fixes https://github.com/astral-sh/ruff/issues/18298
2025-06-20 15:04:51 -04:00
Hmvp
49763a7f7c [flake8-logging] Avoid false positive for exc_info=True outside logger.exception (LOG014) (#18737)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

Fixes https://github.com/astral-sh/ruff/issues/18726 by also checking if
its a literal and not only that it is truthy. See also the first comment
in the issue.

It would have been nice to check for inheritance of BaseException but I
figured that is not possible yet...

## Test Plan

I added a few tests for valid input to exc_info
2025-06-20 14:43:08 -04:00
GiGaGon
2d25aaeaa2 [flake8-pie] Small docs fix to PIE794 (#18829)
<!--
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 noticed this since my code for finding missing safety fix sections
flagged it, there is a missing `/` causing part of the new changes to be
a normal comment instead of a doc comment

## Test Plan

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

N/A, no functionality/tests affected
2025-06-20 14:38:13 -04:00
Gideon
2910988b06 [pylint] Ignore __init__.py files in (PLC0414) (#18400)
## Summary

Ignore `__init__.py` files in `useless-import-alias` (PLC0414).
See discussion in #18365 and #6294: we want to allow redundant aliases
in `__init__.py` files, as they're almost always intentional explicit
re-exports.
Closes #18365
 Closes #6294

---------

Co-authored-by: Dylan <dylwil3@gmail.com>
2025-06-20 18:20:27 +00:00
Brent Westbrook
8cff77c82e Avoid generating diagnostics with per-file ignores (#18801)
## Summary

This PR avoids one of the three calls to `NoqaCode::rule` from
https://github.com/astral-sh/ruff/pull/18391 by applying per-file
ignores in the `LintContext`. To help with this, it also replaces all
direct uses of `LinterSettings.rules.enabled` with a
`LintContext::enabled` (or `Checker::enabled`, which defers to its
context) method. There are still some direct accesses to
`settings.rules`, but as far as I can tell these are not in a part of
the code where we can really access a `LintContext`. I believe all of
the code reachable from `check_path`, where the replaced per-file ignore
code was, should be converted to the new methods.

## Test Plan

Existing tests, with a single snapshot updated for RUF100, which I think
actually shows a more accurate diagnostic message now.
2025-06-20 13:33:09 -04:00
Victor Hugo Gomes
ffb09c84f2 [flake8-simplify] Fix false negatives for shadowed bindings (SIM910, SIM911) (#18794)
<!--
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
I also noticed that the tests for SIM911 were note being run, so I fixed
that.

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

## Test Plan
Add regression test
<!-- How was it tested? -->
2025-06-20 13:25:36 -04:00
Alex Waygood
dc160c4a49 [ty] Fix panics when pulling types for ClassVar or Final parameterized with >1 argument (#18824) 2025-06-20 18:06:40 +01:00
Yunchi Pang
073a71ca9a [pylint] add fix safety section (PLR1714) (#18415)
parent #15584
fix was introduced in #7910

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-06-20 15:40:44 +00:00
GiGaGon
23a3b6ef23 [Perflint] Small docs improvement to PERF401 (#18786)
<!--
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 reading the docs I noticed this paragraph on `PERF401`. It was
added in the same PR that the bug with `:=` was fixed, #15050, but don't
know why it was added. The fix should already take care of adding the
parenthesis, so having this paragraph in the docs is just confusing
since it sounds like the user has to do something.

## Test Plan

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

N/A, no tests/functionality affected
2025-06-20 09:55:49 -05:00
Gene Parmesan Thomas
5b3a501fae [pylint] Avoid flattening nested min/max when outer call has single argument (PLW3301) (#16885)
## Summary

Fixes false positives (and incorrect autofixes) in `nested-min-max`
(`PLW3301`) when the outer `min`/`max` call only has a single argument.
Previously the rule would flatten:

```python
min(min([2, 3], [4, 1]))
```

into `min([2, 3], [4, 1])`, changing the semantics. The rule now skips
any nested call when the outer call has only one positional argument.
The pylint fixture and snapshot were updated accordingly.

## Test Plan

Ran Ruff against the updated `nested_min_max.py` fixture:

```shell
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pylint/nested_min_max.py --no-cache --select=PLW3301 --preview
```

to verify that `min(min([2, 3], [4, 1]))` and `max(max([2, 4], [3, 1]))`
are no longer flagged. Updated the fixture and snapshot; all other
existing warnings remain unchanged. The code compiles and the unit tests
pass.

---

This PR was generated by an AI system in collaboration with maintainers:
@carljm, @ntBre

Fixes #16163

---------

Signed-off-by: Gene Parmesan Thomas <201852096+gopoto@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-06-20 09:35:09 -04:00
Deric Crago
e66f182045 [ruff] Added cls.__dict__.get('__annotations__') check (RUF063) (#18233)
Added `cls.__dict__.get('__annotations__')` check for Python 3.10+ and
Python < 3.10 with `typing-extensions` enabled.

Closes #17853 

<!--
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

Added `cls.__dict__.get('__annotations__')` check for Python 3.10+ and
Python < 3.10 with `typing-extensions` enabled.

## Test Plan

`cargo test`

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-20 09:32:40 -04:00
Micha Reiser
f544026b81 [ty] Use HashTable in PlaceTable (#18819) 2025-06-20 15:31:54 +02:00
Gene Parmesan Thomas
234d248730 docs: Correct collections-named-tuple example to use PascalCase assignment (#16884) 2025-06-20 13:30:46 +00:00
David Peter
0ef324b2dd [ty] ecosystem-analyzer workflow (#18719)
## Summary

Adds a new ecosystem-analyzer workflow with a similar purpose to the
mypy-primer workflow. It creates a richer ecosystem diff report using
[ecosystem-analyzer](https://github.com/astral-sh/ecosystem-analyzer/)
([example
report](https://shark.fish/diff-attr-subscript-narrowing.html)). This is
still experimental and also quite a bit slower than mypy_primer, so I
chose to make this opt-in for now via a `ecosystem-analyzer` label. This
would give us a way to play with this while still evaluating if we
should further invest in this or not.

Advantages over the mypy_primer diff output:
- Interactive filtering of diagnostics
- Statistics overview which breaks down added/removed/changed
diagnostics across lint rules
- Has the concept of "changed" diagnostics, which makes it easier to
review changes where diagnostic messages have changed (along with other
changes).
- Compute diff based on old and new project-lists (`good.txt`). This
allows us to diff changes to the project list itself. This has caused
confusion in the past where we tried to add new projects to `good.txt`,
but then ran the `main`-branch version of ty on that new list (where the
bug was not yet fixed)

Disadvantages:
- The report currently needs to be downloaded from the workflow run, as
I don't know if we have a way of deploying HTML files like this
temporarily to some hosted infrastructure.
2025-06-20 15:23:22 +02:00
med1844
7982edac90 [ty] Add support for @staticmethods (#18809)
## Summary

Add support for `@staticmethod`s. Overall, the changes are very similar
to #16305.

#18587 will be dependent on this PR for a potential fix of
https://github.com/astral-sh/ty/issues/207.

mypy_primer will look bad since the new code allows ty to check more
code.

## Test Plan

Added new markdown tests. Please comment if there's any missing tests
that I should add in, thank you.
2025-06-20 10:38:17 +02:00
Andrej
e180975226 unnecessary_dict_kwargs doc - a note on type checking benefits (#18666) 2025-06-20 08:27:51 +02:00
Victor Hugo Gomes
97819f8a37 [flake8-pytest-style] Mark autofix for PT001 and PT023 as unsafe if there's comments in the decorator (#18792) 2025-06-20 08:23:59 +02:00
Dhruv Manilawala
22177e6915 [ty] Surface matched overload diagnostic directly (#18452)
## Summary

This PR resolves the way diagnostics are reported for an invalid call to
an overloaded function.

If any of the steps in the overload call evaluation algorithm yields a
matching overload but it's type checking that failed, the
`no-matching-overload` diagnostic is incorrect because there is a
matching overload, it's the arguments passed that are invalid as per the
signature. So, this PR improves that by surfacing the diagnostics on the
matching overload directly.

It also provides additional context, specifically the matching overload
where this error occurred and other non-matching overloads. Consider the
following example:

```py
from typing import overload


@overload
def f() -> None: ...
@overload
def f(x: int) -> int: ...
@overload
def f(x: int, y: int) -> int: ...
def f(x: int | None = None, y: int | None = None) -> int | None:
    return None


f("a")
```

We get:

<img width="857" alt="Screenshot 2025-06-18 at 11 07 10"
src="https://github.com/user-attachments/assets/8dbcaf13-2a74-4661-aa94-1225c9402ea6"
/>


## Test Plan

Update test cases, resolve existing todos and validate the updated
snapshots.
2025-06-20 08:36:49 +05:30
InSync
20d73dd41c [ty] Report when a dataclass contains more than one KW_ONLY field (#18731)
## Summary

Part of [#111](https://github.com/astral-sh/ty/issues/111).

After this change, dataclasses with two or more `KW_ONLY` field will be
reported as invalid. The duplicate fields will simply be ignored when
computing `__init__`'s signature.

## Test Plan

Markdown tests.
2025-06-19 19:42:31 -07:00
GiGaGon
50bf3fa45a [flake8-pie] Add fix safety section to PIE794 (#18802)
<!--
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? -->

Part of #15584

This PR adds a fix safety section to `PIE794`

I could not track down when this rule was initially implemented/made
unsafe due how old it could be + multiple large refactors to `ruff`.

There is no comment/reasoning in the code given for the unsafety.

Here is a code example demonstrating why it should be unsafe, since
removing any of the assignments would change program behavior
[playground](https://play.ruff.rs/01004644-4259-4449-a581-5007cd59846a)
```py
class A:
    x = 1
    x = 2
    print(x)

class B:
    x = print(3)
    x = print(4)

class C:
    x = [1,2,3]
    y = x
    x = y[1]
```

## Test Plan

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

N/A, no tests affected.

---------

Co-authored-by: Dylan <dylwil3@gmail.com>
2025-06-19 21:51:23 +00:00
GiGaGon
440635cbe6 [pycodestyle] Add fix safety section to W291 and W293 (#18800)
Part of #15584

This PR adds fix safety sections to `W291` and `W293`

The unsafe caveat was added in #10049


10a1d9f01e/crates/ruff_linter/src/rules/pycodestyle/rules/trailing_whitespace.rs (L92)

Code example demonstrating unsafety:
```
PS ~\Desktop\New_folder\ruff>Get-Content issue.py
```
```py
# W291
"""
1
"""

# W293
"""

"""
```
```
PS ~\Desktop\New_folder\ruff>Get-Escaped-Content issue.py
```
```
# W291\n"""\n1 \n"""\n\n# W293\n"""\n \n"""\r\n
```
```
PS ~\Desktop\New_folder\ruff>uvx ruff check issue.py --isolated --select W
```
```snap
issue.py:3:2: W291 Trailing whitespace
  |
1 | # W291
2 | """
3 | 1
  |  ^ W291
4 | """
  |
  = help: Remove trailing whitespace

issue.py:8:1: W293 Blank line contains whitespace
  |
6 | # W293
7 | """
8 |
  | ^ W293
9 | """
  |
  = help: Remove whitespace from blank line

Found 2 errors.
No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option).
```

## Test Plan

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

N/A, no tests affected.
2025-06-19 16:48:02 -05:00
Dylan
ce0a32aadb [flake8-comprehensions] Handle template strings for comprehension fixes (#18710)
Essentially this PR ensures that when we do fixes like this:

```diff
- t"{set(f(x) for x in foo)}"
+ t"{ {f(x) for x in foo} }"
```
we are correctly adding whitespace around the braces. 

This logic is already in place for f-strings and just needed to be
generalized to interpolated strings.
2025-06-19 16:23:46 -05:00
Brent Westbrook
10a1d9f01e Unify OldDiagnostic and Message (#18391)
Summary
--

This PR unifies the remaining differences between `OldDiagnostic` and
`Message` (`OldDiagnostic` was only missing an optional `noqa_offset`
field) and
replaces `Message` with `OldDiagnostic`.

The biggest functional difference is that the combined `OldDiagnostic`
kind no
longer implements `AsRule` for an infallible conversion to `Rule`. This
was
pretty easy to work around with `is_some_and` and `is_none_or` in the
few places
it was needed. In `LintContext::report_diagnostic_if_enabled` we can
just use
the new `Violation::rule` method, which takes care of most cases.

Most of the interesting changes are in [this
range](8156992540)
before I started renaming.

Test Plan
--

Existing tests

Future Work
--

I think it's time to start shifting some of these fields to the new
`Diagnostic`
kind. I believe we want `Fix` for sure, but I'm less sure about the
others. We
may want to keep a thin wrapper type here anyway to implement a `rule`
method,
so we could leave some of these fields on that too.
2025-06-19 09:37:58 -04:00
Robsdedude
4e83db4d40 [pylint] Detect more exotic NaN literals in PLW0177 (#18630)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-19 11:05:06 +00:00
Victor Hugo Gomes
136443b71b [flake8-async] Mark autofix for ASYNC115 as unsafe if the call expression contains comments (#18753)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-19 13:01:33 +02:00
Victor Hugo Gomes
f7a741a99e [flake8-bugbear] Mark autofix for B004 as unsafe if the hasattr call expr contains comments (#18755) 2025-06-19 10:46:53 +00:00
Charlie Marsh
4c8d612120 Enforce pytest import for decorators (#18779) 2025-06-19 09:49:34 +00:00
Victor Hugo Gomes
65b288b45b [flake8-comprehension] Mark autofix for C420 as unsafe if there's comments inside the dict comprehension (#18768) 2025-06-19 09:43:05 +00:00
chiri
06da2c808f [flake8-async] fix detection for large integer sleep durations in ASYNC116 rule (#18767)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-19 09:37:20 +00:00
renovate[bot]
55a2ff91c7 Update dependency ruff to v0.12.0 (#18790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 09:32:47 +00:00
renovate[bot]
d6705f4700 Update taiki-e/install-action action to v2.53.2 (#18789)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 09:31:46 +00:00
Frazer McLean
f67ff33177 Add lint rule for calling chmod with non-octal integers (#18541)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-19 11:30:29 +02:00
Nikolas Hearp
dcf0a8d4d7 Mark RET501 fix unsafe if comments are inside (#18780)
Co-authored-by: Charlie Marsh <crmarsh416@gmail.com>
2025-06-19 11:12:12 +02:00
724 changed files with 21260 additions and 12390 deletions

View File

@@ -49,7 +49,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build sdist"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
with:
command: sdist
args: --out dist
@@ -79,7 +79,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - x86_64"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
with:
target: x86_64
args: --release --locked --out dist
@@ -121,7 +121,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - aarch64"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
with:
target: aarch64
args: --release --locked --out dist
@@ -177,7 +177,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
with:
target: ${{ matrix.platform.target }}
args: --release --locked --out dist
@@ -230,7 +230,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
with:
target: ${{ matrix.target }}
manylinux: auto
@@ -304,7 +304,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
with:
target: ${{ matrix.platform.target }}
manylinux: auto
@@ -370,7 +370,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
@@ -435,7 +435,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2

View File

@@ -38,7 +38,7 @@ jobs:
submodules: recursive
persist-credentials: false
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
@@ -119,7 +119,7 @@ jobs:
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Extract metadata (tags, labels) for Docker
id: meta
@@ -167,7 +167,7 @@ jobs:
- debian:bookworm-slim,bookworm-slim,debian-slim
- buildpack-deps:bookworm,bookworm,debian
steps:
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
@@ -262,7 +262,7 @@ jobs:
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Extract metadata (tags, labels) for Docker
id: meta

View File

@@ -238,13 +238,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@b3958095189f34b95d402a680b6e96b7f194f7b9 # v1
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-insta
- name: ty mdtests (GitHub annotations)
@@ -296,13 +296,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@b3958095189f34b95d402a680b6e96b7f194f7b9 # v1
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-insta
- name: "Run tests"
@@ -325,7 +325,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-nextest
- name: "Run tests"
@@ -381,7 +381,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@b3958095189f34b95d402a680b6e96b7f194f7b9 # v1
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- name: "Build"
run: cargo build --release --locked
@@ -406,13 +406,13 @@ jobs:
MSRV: ${{ steps.msrv.outputs.value }}
run: rustup default "${MSRV}"
- name: "Install mold"
uses: rui314/setup-mold@b3958095189f34b95d402a680b6e96b7f194f7b9 # v1
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-insta
- name: "Run tests"
@@ -438,7 +438,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@ea65a39d2dcca142c53bddd3a097a674e903f475 # v1.12.7
uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -460,7 +460,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download Ruff binary to test
id: download-cached-binary
@@ -661,7 +661,7 @@ jobs:
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
- name: Fuzz
env:
FORCE_COLOR: 1
@@ -691,7 +691,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@ea65a39d2dcca142c53bddd3a097a674e903f475 # v1.12.7
- uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -712,7 +712,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@35be3186fc8e037e329f06b68dcd807d83dcc6dc # v1.49.2
with:
args: --out dist
- name: "Test wheel"
@@ -731,7 +731,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
@@ -774,7 +774,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -906,13 +906,13 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-codspeed
@@ -939,13 +939,13 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4
uses: taiki-e/install-action@d12e869b89167df346dd0ff65da342d1fb1202fb # v2.53.2
with:
tool: cargo-codspeed

View File

@@ -34,11 +34,11 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
- uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@b3958095189f34b95d402a680b6e96b7f194f7b9 # v1
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: Build ruff
# A debug build means the script runs slower once it gets started,

View File

@@ -37,7 +37,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
@@ -70,7 +70,7 @@ jobs:
echo "Project selector: $PRIMER_SELECTOR"
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
uvx \
--from="git+https://github.com/hauntsaninja/mypy_primer@01a7ca325f674433c58e02416a867178d1571128" \
--from="git+https://github.com/hauntsaninja/mypy_primer@e5f55447969d33ae3c7ccdb183e2a37101867270" \
mypy_primer \
--repo ruff \
--type-checker ty \

View File

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

View File

@@ -0,0 +1,94 @@
name: ty ecosystem-analyzer
permissions: {}
on:
pull_request:
types: [labeled]
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
REF_NAME: ${{ github.ref_name }}
jobs:
ty-ecosystem-analyzer:
name: Compute diagnostic diff
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
if: contains(github.event.label.name, 'ecosystem-analyzer')
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
workspaces: "ruff"
- name: Install Rust toolchain
run: rustup show
- name: Compute diagnostic diff
shell: bash
run: |
cd ruff
echo "Enabling configuration overloads (see .github/mypy-primer-ty.toml)"
mkdir -p ~/.config/ty
cp .github/mypy-primer-ty.toml ~/.config/ty/ty.toml
echo "new commit"
git checkout -b new_commit "$GITHUB_SHA"
git rev-list --format=%s --max-count=1 new_commit
cp crates/ty_python_semantic/resources/primer/good.txt projects_new.txt
echo "old commit (merge base)"
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
git checkout -b old_commit "$MERGE_BASE"
git rev-list --format=%s --max-count=1 old_commit
cp crates/ty_python_semantic/resources/primer/good.txt projects_old.txt
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@9c34dc514ee9aef6735db1dfebb80f63acbc3440"
ecosystem-analyzer \
--repository ruff \
analyze \
--projects ruff/projects_old.txt \
--commit old_commit \
--output diagnostics_old.json
ecosystem-analyzer \
--repository ruff \
analyze \
--projects ruff/projects_new.txt \
--commit new_commit \
--output diagnostics_new.json
ecosystem-analyzer \
generate-diff \
diagnostics_old.json \
diagnostics_new.json \
--old-name "main (merge base)" \
--new-name "$REF_NAME" \
--output-html diff.html
- name: Upload HTML diff report
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: diff.html
path: diff.html

49
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"
@@ -1663,9 +1640,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.173"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "libcst"
@@ -1694,9 +1671,9 @@ dependencies = [
[[package]]
name = "libmimalloc-sys"
version = "0.1.42"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d"
dependencies = [
"cc",
"libc",
@@ -1825,9 +1802,9 @@ dependencies = [
[[package]]
name = "mimalloc"
version = "0.1.46"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40"
dependencies = [
"libmimalloc-sys",
]
@@ -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"
@@ -3603,9 +3570,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.103"
version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [
"proc-macro2",
"quote",

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

@@ -19,7 +19,7 @@ use tempfile::NamedTempFile;
use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_diagnostics::Fix;
use ruff_linter::message::Message;
use ruff_linter::message::OldDiagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::{VERSION, warn_user};
use ruff_macros::CacheKey;
@@ -341,16 +341,16 @@ impl FileCache {
/// Convert the file cache into `Diagnostics`, using `path` as file name.
pub(crate) fn to_diagnostics(&self, path: &Path) -> Option<Diagnostics> {
self.data.lint.as_ref().map(|lint| {
let messages = if lint.messages.is_empty() {
let diagnostics = if lint.messages.is_empty() {
Vec::new()
} else {
let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish();
lint.messages
.iter()
.map(|msg| {
Message::diagnostic(
msg.body.clone(),
msg.suggestion.clone(),
OldDiagnostic::lint(
&msg.body,
msg.suggestion.as_ref(),
msg.range,
msg.fix.clone(),
msg.parent,
@@ -366,7 +366,7 @@ impl FileCache {
} else {
FxHashMap::default()
};
Diagnostics::new(messages, notebook_indexes)
Diagnostics::new(diagnostics, notebook_indexes)
})
}
}
@@ -427,17 +427,17 @@ pub(crate) struct LintCacheData {
}
impl LintCacheData {
pub(crate) fn from_messages(
messages: &[Message],
pub(crate) fn from_diagnostics(
diagnostics: &[OldDiagnostic],
notebook_index: Option<NotebookIndex>,
) -> Self {
let source = if let Some(msg) = messages.first() {
let source = if let Some(msg) = diagnostics.first() {
msg.source_file().source_text().to_owned()
} else {
String::new() // No messages, no need to keep the source!
};
let messages = messages
let messages = diagnostics
.iter()
// 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
@@ -447,7 +447,7 @@ impl LintCacheData {
// Make sure that all message use the same source file.
assert_eq!(
msg.source_file(),
messages.first().unwrap().source_file(),
diagnostics.first().unwrap().source_file(),
"message uses a different source file"
);
CacheMessage {
@@ -612,7 +612,7 @@ mod tests {
use test_case::test_case;
use ruff_cache::CACHE_DIR_NAME;
use ruff_linter::message::Message;
use ruff_linter::message::OldDiagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::settings::flags;
use ruff_linter::settings::types::UnsafeFixes;
@@ -680,7 +680,7 @@ mod tests {
UnsafeFixes::Enabled,
)
.unwrap();
if diagnostics.messages.iter().any(Message::is_syntax_error) {
if diagnostics.inner.iter().any(OldDiagnostic::is_syntax_error) {
parse_errors.push(path.clone());
}
paths.push(path);

View File

@@ -13,7 +13,6 @@ use rustc_hash::FxHashMap;
use ruff_db::panic::catch_unwind;
use ruff_linter::OldDiagnostic;
use ruff_linter::message::Message;
use ruff_linter::package::PackageRoot;
use ruff_linter::registry::Rule;
use ruff_linter::settings::types::UnsafeFixes;
@@ -130,9 +129,10 @@ pub(crate) fn check(
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
Diagnostics::new(
vec![Message::from_diagnostic(
OldDiagnostic::new(IOError { message }, TextRange::default(), &dummy),
None,
vec![OldDiagnostic::new(
IOError { message },
TextRange::default(),
&dummy,
)],
FxHashMap::default(),
)
@@ -166,7 +166,7 @@ pub(crate) fn check(
|a, b| (a.0 + b.0, a.1 + b.1),
);
all_diagnostics.messages.sort();
all_diagnostics.inner.sort();
// Store the caches.
caches.persist()?;
@@ -283,7 +283,7 @@ mod test {
.with_show_fix_status(true)
.emit(
&mut output,
&diagnostics.messages,
&diagnostics.inner,
&EmitterContext::new(&FxHashMap::default()),
)
.unwrap();

View File

@@ -52,6 +52,6 @@ pub(crate) fn check_stdin(
noqa,
fix_mode,
)?;
diagnostics.messages.sort_unstable();
diagnostics.inner.sort_unstable();
Ok(diagnostics)
}

View File

@@ -15,7 +15,6 @@ use rustc_hash::FxHashMap;
use ruff_linter::OldDiagnostic;
use ruff_linter::codes::Rule;
use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only};
use ruff_linter::message::Message;
use ruff_linter::package::PackageRoot;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::settings::types::UnsafeFixes;
@@ -32,18 +31,18 @@ use crate::cache::{Cache, FileCacheKey, LintCacheData};
#[derive(Debug, Default, PartialEq)]
pub(crate) struct Diagnostics {
pub(crate) messages: Vec<Message>,
pub(crate) inner: Vec<OldDiagnostic>,
pub(crate) fixed: FixMap,
pub(crate) notebook_indexes: FxHashMap<String, NotebookIndex>,
}
impl Diagnostics {
pub(crate) fn new(
messages: Vec<Message>,
diagnostics: Vec<OldDiagnostic>,
notebook_indexes: FxHashMap<String, NotebookIndex>,
) -> Self {
Self {
messages,
inner: diagnostics,
fixed: FixMap::default(),
notebook_indexes,
}
@@ -63,15 +62,12 @@ impl Diagnostics {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let source_file = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Message::from_diagnostic(
OldDiagnostic::new(
IOError {
message: err.to_string(),
},
TextRange::default(),
&source_file,
),
None,
vec![OldDiagnostic::new(
IOError {
message: err.to_string(),
},
TextRange::default(),
&source_file,
)],
FxHashMap::default(),
)
@@ -102,7 +98,11 @@ impl Diagnostics {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let dummy = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Message::syntax_error(err, TextRange::default(), dummy)],
vec![OldDiagnostic::syntax_error(
err,
TextRange::default(),
dummy,
)],
FxHashMap::default(),
)
}
@@ -121,7 +121,7 @@ impl Add for Diagnostics {
impl AddAssign for Diagnostics {
fn add_assign(&mut self, other: Self) {
self.messages.extend(other.messages);
self.inner.extend(other.inner);
self.fixed += other.fixed;
self.notebook_indexes.extend(other.notebook_indexes);
}
@@ -202,7 +202,7 @@ pub(crate) fn lint_path(
if match fix_mode {
flags::FixMode::Generate => true,
flags::FixMode::Apply | flags::FixMode::Diff => {
diagnostics.messages.is_empty() && diagnostics.fixed.is_empty()
diagnostics.inner.is_empty() && diagnostics.fixed.is_empty()
}
} {
return Ok(diagnostics);
@@ -222,7 +222,7 @@ pub(crate) fn lint_path(
Some(source_type) => source_type,
None => match SourceType::from(path) {
SourceType::Toml(TomlSourceType::Pyproject) => {
let messages = if settings
let diagnostics = if settings
.rules
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_pyproject_toml())
@@ -240,7 +240,7 @@ pub(crate) fn lint_path(
vec![]
};
return Ok(Diagnostics {
messages,
inner: diagnostics,
..Diagnostics::default()
});
}
@@ -324,7 +324,7 @@ pub(crate) fn lint_path(
};
let has_error = result.has_syntax_errors();
let messages = result.messages;
let diagnostics = result.diagnostics;
if let Some((cache, relative_path, key)) = caching {
// We don't cache parsing errors.
@@ -335,14 +335,14 @@ pub(crate) fn lint_path(
if match fix_mode {
flags::FixMode::Generate => true,
flags::FixMode::Apply | flags::FixMode::Diff => {
messages.is_empty() && fixed.is_empty()
diagnostics.is_empty() && fixed.is_empty()
}
} {
cache.update_lint(
relative_path.to_owned(),
&key,
LintCacheData::from_messages(
&messages,
LintCacheData::from_diagnostics(
&diagnostics,
transformed.as_ipy_notebook().map(Notebook::index).cloned(),
),
);
@@ -357,7 +357,7 @@ pub(crate) fn lint_path(
};
Ok(Diagnostics {
messages,
inner: diagnostics,
fixed: FixMap::from_iter([(fs::relativize_path(path), fixed)]),
notebook_indexes,
})
@@ -396,7 +396,7 @@ pub(crate) fn lint_stdin(
}
return Ok(Diagnostics {
messages: lint_pyproject_toml(&source_file, &settings.linter),
inner: lint_pyproject_toml(&source_file, &settings.linter),
fixed: FixMap::from_iter([(fs::relativize_path(path), FixTable::default())]),
notebook_indexes: FxHashMap::default(),
});
@@ -417,7 +417,7 @@ pub(crate) fn lint_stdin(
};
// Lint the inputs.
let (LinterResult { messages, .. }, transformed, fixed) =
let (LinterResult { diagnostics, .. }, transformed, fixed) =
if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
if let Ok(FixerResult {
result,
@@ -501,7 +501,7 @@ pub(crate) fn lint_stdin(
};
Ok(Diagnostics {
messages,
inner: diagnostics,
fixed: FixMap::from_iter([(
fs::relativize_path(path.unwrap_or_else(|| Path::new("-"))),
fixed,

View File

@@ -363,7 +363,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
Printer::clear_screen()?;
printer.write_to_user("Starting linter in watch mode...\n");
let messages = commands::check::check(
let diagnostics = commands::check::check(
&files,
&pyproject_config,
&config_arguments,
@@ -372,7 +372,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
fix_mode,
unsafe_fixes,
)?;
printer.write_continuously(&mut writer, &messages, preview)?;
printer.write_continuously(&mut writer, &diagnostics, preview)?;
// In watch mode, we may need to re-resolve the configuration.
// TODO(charlie): Re-compute other derivative values, like the `printer`.
@@ -392,7 +392,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
Printer::clear_screen()?;
printer.write_to_user("File change detected...\n");
let messages = commands::check::check(
let diagnostics = commands::check::check(
&files,
&pyproject_config,
&config_arguments,
@@ -401,7 +401,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
fix_mode,
unsafe_fixes,
)?;
printer.write_continuously(&mut writer, &messages, preview)?;
printer.write_continuously(&mut writer, &diagnostics, preview)?;
}
Err(err) => return Err(err.into()),
}
@@ -463,11 +463,11 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
// there are any violations, unless we're explicitly asked to exit zero on
// fix.
if cli.exit_non_zero_on_fix {
if !diagnostics.fixed.is_empty() || !diagnostics.messages.is_empty() {
if !diagnostics.fixed.is_empty() || !diagnostics.inner.is_empty() {
return Ok(ExitStatus::Failure);
}
} else {
if !diagnostics.messages.is_empty() {
if !diagnostics.inner.is_empty() {
return Ok(ExitStatus::Failure);
}
}

View File

@@ -14,7 +14,7 @@ use ruff_linter::fs::relativize_path;
use ruff_linter::logging::LogLevel;
use ruff_linter::message::{
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
JsonEmitter, JsonLinesEmitter, JunitEmitter, Message, PylintEmitter, RdjsonEmitter,
JsonEmitter, JsonLinesEmitter, JunitEmitter, OldDiagnostic, PylintEmitter, RdjsonEmitter,
SarifEmitter, TextEmitter,
};
use ruff_linter::notify_user;
@@ -85,7 +85,7 @@ impl Printer {
.sum::<usize>();
if self.flags.intersects(Flags::SHOW_VIOLATIONS) {
let remaining = diagnostics.messages.len();
let remaining = diagnostics.inner.len();
let total = fixed + remaining;
if fixed > 0 {
let s = if total == 1 { "" } else { "s" };
@@ -229,16 +229,16 @@ impl Printer {
match self.format {
OutputFormat::Json => {
JsonEmitter.emit(writer, &diagnostics.messages, &context)?;
JsonEmitter.emit(writer, &diagnostics.inner, &context)?;
}
OutputFormat::Rdjson => {
RdjsonEmitter.emit(writer, &diagnostics.messages, &context)?;
RdjsonEmitter.emit(writer, &diagnostics.inner, &context)?;
}
OutputFormat::JsonLines => {
JsonLinesEmitter.emit(writer, &diagnostics.messages, &context)?;
JsonLinesEmitter.emit(writer, &diagnostics.inner, &context)?;
}
OutputFormat::Junit => {
JunitEmitter.emit(writer, &diagnostics.messages, &context)?;
JunitEmitter.emit(writer, &diagnostics.inner, &context)?;
}
OutputFormat::Concise | OutputFormat::Full => {
TextEmitter::default()
@@ -246,7 +246,7 @@ impl Printer {
.with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
.with_show_source(self.format == OutputFormat::Full)
.with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?;
.emit(writer, &diagnostics.inner, &context)?;
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
if !diagnostics.fixed.is_empty() {
@@ -262,7 +262,7 @@ impl Printer {
GroupedEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?;
.emit(writer, &diagnostics.inner, &context)?;
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
if !diagnostics.fixed.is_empty() {
@@ -274,19 +274,19 @@ impl Printer {
self.write_summary_text(writer, diagnostics)?;
}
OutputFormat::Github => {
GithubEmitter.emit(writer, &diagnostics.messages, &context)?;
GithubEmitter.emit(writer, &diagnostics.inner, &context)?;
}
OutputFormat::Gitlab => {
GitlabEmitter::default().emit(writer, &diagnostics.messages, &context)?;
GitlabEmitter::default().emit(writer, &diagnostics.inner, &context)?;
}
OutputFormat::Pylint => {
PylintEmitter.emit(writer, &diagnostics.messages, &context)?;
PylintEmitter.emit(writer, &diagnostics.inner, &context)?;
}
OutputFormat::Azure => {
AzureEmitter.emit(writer, &diagnostics.messages, &context)?;
AzureEmitter.emit(writer, &diagnostics.inner, &context)?;
}
OutputFormat::Sarif => {
SarifEmitter.emit(writer, &diagnostics.messages, &context)?;
SarifEmitter.emit(writer, &diagnostics.inner, &context)?;
}
}
@@ -301,13 +301,13 @@ impl Printer {
writer: &mut dyn Write,
) -> Result<()> {
let statistics: Vec<ExpandedStatistics> = diagnostics
.messages
.inner
.iter()
.map(|message| (message.noqa_code(), message))
.sorted_by_key(|(code, message)| (*code, message.fixable()))
.fold(
vec![],
|mut acc: Vec<((Option<NoqaCode>, &Message), usize)>, (code, message)| {
|mut acc: Vec<((Option<NoqaCode>, &OldDiagnostic), usize)>, (code, message)| {
if let Some(((prev_code, _prev_message), count)) = acc.last_mut() {
if *prev_code == code {
*count += 1;
@@ -416,20 +416,20 @@ impl Printer {
}
if self.log_level >= LogLevel::Default {
let s = if diagnostics.messages.len() == 1 {
let s = if diagnostics.inner.len() == 1 {
""
} else {
"s"
};
notify_user!(
"Found {} error{s}. Watching for file changes.",
diagnostics.messages.len()
diagnostics.inner.len()
);
}
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
if !diagnostics.messages.is_empty() {
if !diagnostics.inner.is_empty() {
if self.log_level >= LogLevel::Default {
writeln!(writer)?;
}
@@ -439,7 +439,7 @@ impl Printer {
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_show_source(preview)
.with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?;
.emit(writer, &diagnostics.inner, &context)?;
}
writer.flush()?;
@@ -522,7 +522,7 @@ impl FixableStatistics {
let mut applicable = 0;
let mut inapplicable_unsafe = 0;
for message in &diagnostics.messages {
for message in &diagnostics.inner {
if let Some(fix) = message.fix() {
if fix.applies(unsafe_fixes.required_applicability()) {
applicable += 1;

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()
@@ -204,8 +203,23 @@ static SYMPY: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLock::new
)
});
#[bench(args=[&*ALTAIR, &*FREQTRADE, &*PYDANTIC], sample_size=2, sample_count=3)]
fn small(bencher: Bencher, benchmark: &Benchmark) {
static TANJUN: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLock::new(|| {
Benchmark::new(
RealWorldProject {
name: "tanjun",
repository: "https://github.com/FasterSpeeding/Tanjun",
commit: "69f40db188196bc59516b6c69849c2d85fbc2f4a",
paths: vec![SystemPath::new("tanjun")],
dependencies: vec!["hikari", "alluka"],
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
100,
)
});
#[track_caller]
fn run_single_threaded(bencher: Bencher, benchmark: &Benchmark) {
bencher
.with_inputs(|| benchmark.setup_iteration())
.bench_local_refs(|db| {
@@ -213,41 +227,55 @@ fn small(bencher: Bencher, benchmark: &Benchmark) {
});
}
#[bench(args=[&*ALTAIR, &*FREQTRADE, &*PYDANTIC, &*TANJUN], sample_size=2, sample_count=3)]
fn small(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
#[bench(args=[&*COLOUR_SCIENCE, &*PANDAS], sample_size=1, sample_count=3)]
fn medium(bencher: Bencher, benchmark: &Benchmark) {
bencher
.with_inputs(|| benchmark.setup_iteration())
.bench_local_refs(|db| {
check_project(db, benchmark.max_diagnostics);
});
run_single_threaded(bencher, benchmark);
}
#[bench(args=[&*SYMPY], sample_size=1, sample_count=2)]
fn large(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
#[bench(args=[&*PYDANTIC], sample_size=3, sample_count=3)]
fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
let thread_pool = ThreadPoolBuilder::new().build().unwrap();
bencher
.with_inputs(|| benchmark.setup_iteration())
.bench_local_refs(|db| {
check_project(db, benchmark.max_diagnostics);
.bench_local_values(|db| {
thread_pool.install(|| {
check_project(&db, benchmark.max_diagnostics);
db
})
});
}
fn main() {
let filter =
std::env::var("TY_LOG").unwrap_or("ty_walltime=info,ruff_benchmark=info".to_string());
let _logging = setup_logging_with_filter(&filter).expect("Filter to be valid");
// Disable multithreading for now due to
// https://github.com/salsa-rs/salsa/issues/918.
//
// Salsa has a fast-path for the first db when looking up ingredients.
// It seems that this fast-path becomes extremely slow for all db's other
// than the first one, especially when using multithreading (10x slower than the first run).
ThreadPoolBuilder::new()
.num_threads(1)
.use_current_thread()
.build_global()
.unwrap();
let filter =
std::env::var("TY_LOG").unwrap_or("ty_walltime=info,ruff_benchmark=info".to_string());
let _logging = setup_logging_with_filter(&filter).expect("Filter to be valid");
// Salsa uses an optimized lookup for the ingredient index when using only a single database.
// This optimization results in at least a 10% speedup compared to when using multiple databases.
// To reduce noise, run one benchmark so that all benchmarks take the less optimized "not the first db"
// branch when looking up the ingredient index.
{
let db = TANJUN.setup_iteration();
check_project(&db, TANJUN.max_diagnostics);
}
divan::main();
}

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

@@ -36,14 +36,20 @@ impl fmt::Display for AnalyzeSettings {
}
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, CacheKey)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
pub enum Direction {
/// Construct a map from module to its dependencies (i.e., the modules that it imports).
#[default]
#[cfg_attr(feature = "serde", serde(alias = "Dependencies"))]
Dependencies,
/// Construct a map from module to its dependents (i.e., the modules that import it).
#[cfg_attr(feature = "serde", serde(alias = "Dependents"))]
Dependents,
}

View File

@@ -165,3 +165,15 @@ async def test_trio_async115_helpers():
await trio.sleep(seconds=0) # ASYNC115
await trio.sleep(delay=0) # OK
# https://github.com/astral-sh/ruff/issues/18740
# The autofix for this is unsafe due to the comments.
async def func():
import trio
await (
trio # comment
.sleep( # comment
0 # comment
)
)

View File

@@ -128,3 +128,11 @@ async def test_trio_async116_helpers():
await trio.sleep(seconds=86401) # ASYNC116
await trio.sleep(delay=86401) # OK
async def _():
import trio
from trio import sleep
await sleep(18446744073709551616)
await trio.sleep(99999999999999999999)

View File

@@ -29,3 +29,13 @@ def this_is_fine():
o = object()
if callable(o):
print("Ooh, this is actually callable.")
# https://github.com/astral-sh/ruff/issues/18741
# The autofix for this is unsafe due to the comments.
hasattr(
# comment 1
obj, # comment 2
# comment 3
"__call__", # comment 4
# comment 5
)

View File

@@ -36,9 +36,19 @@ set(
# some more
)
# t-strings
print(t"Hello {set(f(a) for a in 'abc')} World")
print(t"Hello { set(f(a) for a in 'abc') } World")
small_nums = t"{set(a if a < 6 else 0 for a in range(3))}"
print(t"Hello {set(a for a in range(3))} World")
print(t"{set(a for a in 'abc') - set(a for a in 'ab')}")
print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }")
# Not built-in set.
def set(*args, **kwargs):
return None
set(2 * x for x in range(3))
set(x for x in range(3))

View File

@@ -34,4 +34,16 @@ s = set( # outer set comment
))))
# Test trailing comma case
s = set([x for x in range(3)],)
s = set([x for x in range(3)],)
s = t"{set([x for x in 'ab'])}"
s = t'{set([x for x in "ab"])}'
def f(x):
return x
s = t"{set([f(x) for x in 'ab'])}"
s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
s = t"{set([x for x in 'ab']) | set([x for x in 'ab'])}"

View File

@@ -24,3 +24,12 @@ f"{set(['a', 'b']) - set(['a'])}"
f"{ set(['a', 'b']) - set(['a']) }"
f"a {set(['a', 'b']) - set(['a'])} b"
f"a { set(['a', 'b']) - set(['a']) } b"
t"{set([1,2,3])}"
t"{set(['a', 'b'])}"
t'{set(["a", "b"])}'
t"{set(['a', 'b']) - set(['a'])}"
t"{ set(['a', 'b']) - set(['a']) }"
t"a {set(['a', 'b']) - set(['a'])} b"
t"a { set(['a', 'b']) - set(['a']) } b"

View File

@@ -27,3 +27,13 @@ dict(
tuple( # comment
)
t"{dict(x='y')}"
t'{dict(x="y")}'
t"{dict()}"
t"a {dict()} b"
t"{dict(x='y') | dict(y='z')}"
t"{ dict(x='y') | dict(y='z') }"
t"a {dict(x='y') | dict(y='z')} b"
t"a { dict(x='y') | dict(y='z') } b"

View File

@@ -70,3 +70,8 @@ list(map(lambda: 1, "xyz"))
list(map(lambda x, y: x, [(1, 2), (3, 4)]))
list(map(lambda: 1, "xyz"))
list(map(lambda x, y: x, [(1, 2), (3, 4)]))
# When inside t-string, then the fix should be surrounded by whitespace
_ = t"{set(map(lambda x: x % 2 == 0, nums))}"
_ = t"{dict(map(lambda v: (v, v**2), nums))}"

View File

@@ -90,3 +90,14 @@ def func():
def func():
{(a, b): a + b for (a, b) in [(1, 2), (3, 4)]} # OK
# https://github.com/astral-sh/ruff/issues/18764
{ # 1
a # 2
: # 3
None # 4
for # 5
a # 6
in # 7
iterable # 8
} # 9

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

@@ -32,6 +32,12 @@ except ...:
### No errors
logging.info("", exc_info=ValueError())
logger.info("", exc_info=ValueError())
logging.info("", exc_info=(exc_type, exc_value, exc_traceback))
logger.info("", exc_info=(exc_type, exc_value, exc_traceback))
logging.info("", exc_info=a)
logger.info("", exc_info=a)

View File

@@ -14,3 +14,6 @@ range(0, 10, 1)
range(0, 10, step=1)
range(start=0, stop=10)
range(0, stop=10)
# regression test for https://github.com/astral-sh/ruff/pull/18805
range((0), 42)

View File

@@ -1,4 +1,5 @@
from typing import (
TYPE_CHECKING,
Union,
)
@@ -90,3 +91,22 @@ class Foo:
def bad5(self, arg: int | (float | complex)) -> None:
...
# https://github.com/astral-sh/ruff/issues/18298
# fix must not yield runtime `None | None | ...` (TypeError)
class Issue18298:
def f1(self, arg: None | int | None | float = None) -> None: # PYI041 - no fix
pass
if TYPE_CHECKING:
def f2(self, arg: None | int | None | float = None) -> None: ... # PYI041 - with fix
else:
def f2(self, arg=None) -> None:
pass
def f3(self, arg: None | float | None | int | None = None) -> None: # PYI041 - with fix
pass

View File

@@ -70,3 +70,11 @@ class Foo:
def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041
def bad5(self, arg: int | (float | complex)) -> None: ... # PYI041
# https://github.com/astral-sh/ruff/issues/18298
# fix must not yield runtime `None | None | ...` (TypeError)
class Issue18298:
def f1(self, arg: None | int | None | float = None) -> None: ... # PYI041 - with fix
def f3(self, arg: None | float | None | int | None = None) -> None: ... # PYI041 - with fix

View File

@@ -0,0 +1,8 @@
from __future__ import annotations
# https://github.com/astral-sh/ruff/issues/18298
# fix must not yield runtime `None | None | ...` (TypeError)
class Issue18298:
def f1(self, arg: None | int | None | float = None) -> None:
pass

View File

@@ -76,3 +76,20 @@ def aliased_parentheses_with_params():
)
def aliased_parentheses_no_params_multiline():
return 42
# https://github.com/astral-sh/ruff/issues/18770
@pytest.fixture(
# TODO: use module scope
# scope="module"
)
def my_fixture(): ...
@(pytest.fixture())
def outer_paren_fixture_no_params():
return 42
@(fixture())
def outer_paren_imported_fixture_no_params():
return 42

View File

@@ -1,3 +1,8 @@
@pytest.mark.foo(scope="module")
def ok_due_to_missing_import():
pass
import pytest
@@ -72,3 +77,25 @@ class TestClass:
@pytest.mark.foo()
def test_something():
pass
# https://github.com/astral-sh/ruff/issues/18770
@pytest.mark.parametrize(
# TODO: fix later
# ("param1", "param2"),
# (
# (1, 2),
# (3, 4),
# ),
)
def test_bar(param1, param2): ...
@(pytest.mark.foo())
def test_outer_paren_mark_function():
pass
class TestClass:
@(pytest.mark.foo())
def test_method_outer_paren():
pass

View File

@@ -105,3 +105,8 @@ if future.exception():
future = executor.submit(float, "a")
if future.exception():
raise future.Exception()
raise TypeError(
# comment
)

View File

@@ -49,3 +49,13 @@ class Baz:
def prop4(self) -> None:
print("I've run out of things to say")
return None
# https://github.com/astral-sh/ruff/issues/18774
class _:
def foo(bar):
if not bar:
return
return (
None # comment
)

View File

@@ -128,3 +128,16 @@ def write_models(directory, Models):
pass; \
\
#
# Regression tests for: https://github.com/astral-sh/ruff/issues/18209
try:
1 / 0
except ():
pass
BaseException = ValueError
try:
int("a")
except BaseException:
pass

View File

@@ -49,3 +49,9 @@ def foo(some_other: object):
# OK
def foo(some_other):
a = some_other.get('a', None)
# https://github.com/astral-sh/ruff/issues/18777
def foo():
dict = {"Tom": 23, "Maria": 23, "Dog": 11}
age = dict.get("Cat", None)

View File

@@ -23,3 +23,14 @@ for k, v in zip(d2.keys(), d2.values()): # SIM911
items = zip(x.keys(), x.values()) # OK
items.bar = zip(x.keys(), x.values()) # OK
# https://github.com/astral-sh/ruff/issues/18777
def foo():
dict = {}
for country, stars in zip(dict.keys(), dict.values()):
...
# https://github.com/astral-sh/ruff/issues/18776
flag_stars = {}
for country, stars in(zip)(flag_stars.keys(), flag_stars.values()):...

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

@@ -85,3 +85,19 @@ import builtins
for i in builtins.list(nested_tuple): # PERF101
pass
# https://github.com/astral-sh/ruff/issues/18783
items = (1, 2, 3)
for i in(list)(items):
print(i)
# https://github.com/astral-sh/ruff/issues/18784
items = (1, 2, 3)
for i in ( # 1
list # 2
# 3
)( # 4
items # 5
# 6
):
print(i)

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

@@ -70,3 +70,9 @@ class D(C):
class E(A, C):
...
# https://github.com/astral-sh/ruff/issues/18814
class Bar(Foo, # 1
Foo # 2
):
pass

View File

@@ -0,0 +1,4 @@
import collections as collections
from collections import OrderedDict as OrderedDict
from . import foo as foo
from .foo import bar as bar

View File

@@ -168,7 +168,7 @@ def github_issue_1879():
def function_returning_function(r):
return function_returning_generator(r)
assert len(function_returning_list(z)) # [PLC1802] differs from pylint
assert len(function_returning_list(z)) # [PLC1802] differs from pylint
assert len(function_returning_int(z))
# This should raise a PLC1802 once astroid can infer it
# See https://github.com/pylint-dev/pylint/pull/3821#issuecomment-743771514
@@ -196,7 +196,7 @@ def f(cond:bool):
def g(cond:bool):
x = [1,2,3]
if cond:
x = [4,5,6]
x = [4,5,6]
if len(x): # this should be addressed
print(x)
del x
@@ -236,3 +236,15 @@ def j():
# regression tests for https://github.com/astral-sh/ruff/issues/14690
bool(len(ascii(1)))
bool(len(sorted("")))
# regression tests for https://github.com/astral-sh/ruff/issues/18811
fruits = []
if(len)(fruits):
...
# regression tests for https://github.com/astral-sh/ruff/issues/18812
fruits = []
if len(
fruits # comment
):
...

View File

@@ -92,3 +92,8 @@ if y == np.inf:
# OK
if x == "nan":
pass
# PLW0117
# https://github.com/astral-sh/ruff/issues/18596
assert x == float("-NaN")
assert x == float(" \n+nan \t")

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),
@@ -55,3 +55,8 @@ max_word_len = max(
*(len(word) for word in "blah blah blah".split(" ")),
len("Done!"),
)
# Outer call has a single argument, inner call has multiple arguments; should not trigger.
min(min([2, 3], [4, 1]))
max(max([2, 4], [3, 1]))

View File

@@ -129,3 +129,6 @@ blah = dict[{"a": 1}.__delitem__("a")] # OK
# https://github.com/astral-sh/ruff/issues/14597
assert "abc".__str__() == "abc"
# https://github.com/astral-sh/ruff/issues/18813
three = 1 if 1 else(3.0).__str__()

View File

@@ -71,3 +71,47 @@ if sys.version_info <= (3, 14, 0):
if sys.version_info <= (3, 14, 15):
print()
# https://github.com/astral-sh/ruff/issues/18165
if sys.version_info.major >= 3:
print("3")
else:
print("2")
if sys.version_info.major > 3:
print("3")
else:
print("2")
if sys.version_info.major <= 3:
print("3")
else:
print("2")
if sys.version_info.major < 3:
print("3")
else:
print("2")
if sys.version_info.major == 3:
print("3")
else:
print("2")
# Semantically incorrect, skip fixing
if sys.version_info.major[1] > 3:
print(3)
else:
print(2)
if sys.version_info.major > (3, 13):
print(3)
else:
print(2)
if sys.version_info.major[:2] > (3, 13):
print(3)
else:
print(2)

View File

@@ -105,3 +105,12 @@ with open("furb129.py") as f:
# Test case for issue #17683 (missing space before keyword)
print([line for line in f.readlines()if True])
# https://github.com/astral-sh/ruff/issues/18843
with open("file.txt") as fp:
for line in ( # 1
fp. # 3 # 2
readlines( # 4
) # 5
):
...

View File

@@ -44,6 +44,13 @@ log(1, math.e)
math.log(1, 2.0001)
math.log(1, 10.0001)
# https://github.com/astral-sh/ruff/issues/18747
def log():
yield math.log((yield), math.e)
def log():
yield math.log((yield from x), math.e)
# see: https://github.com/astral-sh/ruff/issues/18639
math.log(1, 10 # comment

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

@@ -110,3 +110,19 @@ class ShouldMatchB008RuleOfImmutableTypeAnnotationIgnored:
# ignored
this_is_fine: int = f()
# Test for:
# https://github.com/astral-sh/ruff/issues/17424
@dataclass(frozen=True)
class C:
foo: int = 1
@dataclass
class D:
c: C = C()
@dataclass
class E:
c: C = C()

View File

@@ -73,3 +73,55 @@ class IntConversionDescriptor:
@frozen
class InventoryItem:
quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)
# Test for:
# https://github.com/astral-sh/ruff/issues/17424
@frozen
class C:
foo: int = 1
@attr.frozen
class D:
foo: int = 1
@define
class E:
c: C = C()
d: D = D()
@attr.s
class F:
foo: int = 1
@attr.mutable
class G:
foo: int = 1
@attr.attrs
class H:
f: F = F()
g: G = G()
@attr.define
class I:
f: F = F()
g: G = G()
@attr.frozen
class J:
f: F = F()
g: G = G()
@attr.mutable
class K:
f: F = F()
g: G = G()

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

@@ -23,69 +23,69 @@ value2 = my_dict.get("key2", [1, 2, 3]).append(4)
# Valid
# Using dict.get with a falsy fallback: False
value = my_dict.get("key", False)
value = my_dict.get("key", False)
# Using dict.get with a falsy fallback: empty string
value = my_dict.get("key", "")
value = my_dict.get("key", "")
# Using dict.get with a falsy fallback: empty list
value = my_dict.get("key", [])
value = my_dict.get("key", [])
# Using dict.get with a falsy fallback: empty dict
value = my_dict.get("key", {})
value = my_dict.get("key", {})
# Using dict.get with a falsy fallback: empty set
value = my_dict.get("key", set())
value = my_dict.get("key", set())
# Using dict.get with a falsy fallback: zero integer
value = my_dict.get("key", 0)
value = my_dict.get("key", 0)
# Using dict.get with a falsy fallback: zero float
value = my_dict.get("key", 0.0)
value = my_dict.get("key", 0.0)
# Using dict.get with a falsy fallback: None
value = my_dict.get("key", None)
value = my_dict.get("key", None)
# Using dict.get with a falsy fallback via function call
value = my_dict.get("key", list())
value = my_dict.get("key", dict())
value = my_dict.get("key", set())
value = my_dict.get("key", list())
value = my_dict.get("key", dict())
value = my_dict.get("key", set())
# Reassigning with falsy fallback
def get_value(d):
return d.get("key", False)
return d.get("key", False)
# Multiple dict.get calls with mixed fallbacks
value1 = my_dict.get("key1", "default")
value2 = my_dict.get("key2", 0)
value2 = my_dict.get("key2", 0)
value3 = my_dict.get("key3", "another default")
# Using dict.get in a class with falsy fallback
class MyClass:
def method(self):
return self.data.get("key", {})
return self.data.get("key", {})
# Using dict.get in a nested function with falsy fallback
def outer():
def inner():
return my_dict.get("key", "")
return my_dict.get("key", "")
return inner()
# Using dict.get with variable fallback that is falsy
falsy_value = None
value = my_dict.get("key", falsy_value)
value = my_dict.get("key", falsy_value)
# Using dict.get with variable fallback that is truthy
truthy_value = "exists"
value = my_dict.get("key", truthy_value)
# Using dict.get with complex expressions as fallback
value = my_dict.get("key", 0 or "default")
value = my_dict.get("key", [] if condition else {})
value = my_dict.get("key", 0 or "default")
value = my_dict.get("key", [] if condition else {})
# testing dict.get call using kwargs
value = my_dict.get(key="key", default=False)
value = my_dict.get(default=[], key="key")
value = my_dict.get(key="key", default=False)
value = my_dict.get(default=[], key="key")
# Edge Cases
@@ -93,16 +93,16 @@ value = my_dict.get(default=[], key="key")
dicts = [my_dict, my_dict, my_dict]
# Falsy fallback in a lambda
get_fallback = lambda d: d.get("key", False)
get_fallback = lambda d: d.get("key", False)
# Falsy fallback in a list comprehension
results = [d.get("key", "") for d in dicts]
results = [d.get("key", "") for d in dicts]
# Falsy fallback in a generator expression
results = (d.get("key", None) for d in dicts)
results = (d.get("key", None) for d in dicts)
# Falsy fallback in a ternary expression
value = my_dict.get("key", 0) if True else "default"
value = my_dict.get("key", 0) if True else "default"
# Falsy fallback with inline comment
@@ -185,3 +185,7 @@ not my_dict.get(
# TypeError is raised at runtime before and after the fix, but we still bail
# out for having an unrecognized number of arguments
not my_dict.get("key", False, foo=...)
# https://github.com/astral-sh/ruff/issues/18798
d = {}
not d.get("key", (False))

View File

@@ -0,0 +1,18 @@
# RUF063
# Cases that should trigger the violation
foo.__dict__.get("__annotations__") # RUF063
foo.__dict__.get("__annotations__", None) # RUF063
foo.__dict__.get("__annotations__", {}) # RUF063
foo.__dict__["__annotations__"] # RUF063
# Cases that should NOT trigger the violation
foo.__dict__.get("not__annotations__")
foo.__dict__.get("not__annotations__", None)
foo.__dict__.get("not__annotations__", {})
foo.__dict__["not__annotations__"]
foo.__annotations__
foo.get("__annotations__")
foo.get("__annotations__", None)
foo.get("__annotations__", {})

View File

@@ -0,0 +1,53 @@
import dbm.gnu
import dbm.ndbm
import os
from pathlib import Path
os.chmod("foo", 444) # Error
os.chmod("foo", 0o444) # OK
os.chmod("foo", 7777) # Error
os.chmod("foo", 10000) # Error
os.chmod("foo", 99999) # Error
os.umask(777) # Error
os.umask(0o777) # OK
os.fchmod(0, 400) # Error
os.fchmod(0, 0o400) # OK
os.lchmod("foo", 755) # Error
os.lchmod("foo", 0o755) # OK
os.mkdir("foo", 600) # Error
os.mkdir("foo", 0o600) # OK
os.makedirs("foo", 644) # Error
os.makedirs("foo", 0o644) # OK
os.mkfifo("foo", 640) # Error
os.mkfifo("foo", 0o640) # OK
os.mknod("foo", 660) # Error
os.mknod("foo", 0o660) # OK
os.open("foo", os.O_CREAT, 644) # Error
os.open("foo", os.O_CREAT, 0o644) # OK
Path("bar").chmod(755) # Error
Path("bar").chmod(0o755) # OK
path = Path("bar")
path.chmod(755) # Error
path.chmod(0o755) # OK
dbm.open("db", "r", 600) # Error
dbm.open("db", "r", 0o600) # OK
dbm.gnu.open("db", "r", 600) # Error
dbm.gnu.open("db", "r", 0o600) # OK
dbm.ndbm.open("db", "r", 600) # Error
dbm.ndbm.open("db", "r", 0o600) # OK
os.fchmod(0, 256) # 0o400
os.fchmod(0, 493) # 0o755

View File

@@ -10,7 +10,7 @@ use crate::rules::{
/// Run lint rules over the [`Binding`]s.
pub(crate) fn bindings(checker: &Checker) {
if !checker.any_enabled(&[
if !checker.any_rule_enabled(&[
Rule::AssignmentInAssert,
Rule::InvalidAllFormat,
Rule::InvalidAllObject,
@@ -30,11 +30,11 @@ pub(crate) fn bindings(checker: &Checker) {
}
for (binding_id, binding) in checker.semantic.bindings.iter_enumerated() {
if checker.enabled(Rule::UnusedVariable) {
if checker.is_rule_enabled(Rule::UnusedVariable) {
if binding.kind.is_bound_exception()
&& binding.is_unused()
&& !checker
.settings
.settings()
.dummy_variable_rgx
.is_match(binding.name(checker.source()))
{
@@ -54,47 +54,47 @@ pub(crate) fn bindings(checker: &Checker) {
});
}
}
if checker.enabled(Rule::InvalidAllFormat) {
if checker.is_rule_enabled(Rule::InvalidAllFormat) {
pylint::rules::invalid_all_format(checker, binding);
}
if checker.enabled(Rule::InvalidAllObject) {
if checker.is_rule_enabled(Rule::InvalidAllObject) {
pylint::rules::invalid_all_object(checker, binding);
}
if checker.enabled(Rule::NonAsciiName) {
if checker.is_rule_enabled(Rule::NonAsciiName) {
pylint::rules::non_ascii_name(checker, binding);
}
if checker.enabled(Rule::UnconventionalImportAlias) {
if checker.is_rule_enabled(Rule::UnconventionalImportAlias) {
flake8_import_conventions::rules::unconventional_import_alias(
checker,
binding,
&checker.settings.flake8_import_conventions.aliases,
&checker.settings().flake8_import_conventions.aliases,
);
}
if checker.enabled(Rule::UnaliasedCollectionsAbcSetImport) {
if checker.is_rule_enabled(Rule::UnaliasedCollectionsAbcSetImport) {
flake8_pyi::rules::unaliased_collections_abc_set_import(checker, binding);
}
if !checker.source_type.is_stub() && checker.enabled(Rule::UnquotedTypeAlias) {
if !checker.source_type.is_stub() && checker.is_rule_enabled(Rule::UnquotedTypeAlias) {
flake8_type_checking::rules::unquoted_type_alias(checker, binding);
}
if checker.enabled(Rule::UnsortedDunderSlots) {
if checker.is_rule_enabled(Rule::UnsortedDunderSlots) {
ruff::rules::sort_dunder_slots(checker, binding);
}
if checker.enabled(Rule::UsedDummyVariable) {
if checker.is_rule_enabled(Rule::UsedDummyVariable) {
ruff::rules::used_dummy_variable(checker, binding, binding_id);
}
if checker.enabled(Rule::AssignmentInAssert) {
if checker.is_rule_enabled(Rule::AssignmentInAssert) {
ruff::rules::assignment_in_assert(checker, binding);
}
if checker.enabled(Rule::PytestUnittestRaisesAssertion) {
if checker.is_rule_enabled(Rule::PytestUnittestRaisesAssertion) {
flake8_pytest_style::rules::unittest_raises_assertion_binding(checker, binding);
}
if checker.enabled(Rule::ForLoopWrites) {
if checker.is_rule_enabled(Rule::ForLoopWrites) {
refurb::rules::for_loop_writes_binding(checker, binding);
}
if checker.enabled(Rule::CustomTypeVarForSelf) {
if checker.is_rule_enabled(Rule::CustomTypeVarForSelf) {
flake8_pyi::rules::custom_type_var_instead_of_self(checker, binding);
}
if checker.enabled(Rule::PrivateTypeParameter) {
if checker.is_rule_enabled(Rule::PrivateTypeParameter) {
pyupgrade::rules::private_type_parameter(checker, binding);
}
}

View File

@@ -6,10 +6,10 @@ use crate::rules::{flake8_simplify, refurb};
/// Run lint rules over a [`Comprehension`] syntax nodes.
pub(crate) fn comprehension(comprehension: &Comprehension, checker: &Checker) {
if checker.enabled(Rule::InDictKeys) {
if checker.is_rule_enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_comprehension(checker, comprehension);
}
if checker.enabled(Rule::ReadlinesInFor) {
if checker.is_rule_enabled(Rule::ReadlinesInFor) {
refurb::rules::readlines_in_comprehension(checker, comprehension);
}
}

View File

@@ -14,31 +14,31 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
let Stmt::For(stmt_for) = checker.semantic.current_statement() else {
unreachable!("Expected Stmt::For");
};
if checker.enabled(Rule::UnusedLoopControlVariable) {
if checker.is_rule_enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, stmt_for);
}
if checker.enabled(Rule::IncorrectDictIterator) {
if checker.is_rule_enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(checker, stmt_for);
}
if checker.enabled(Rule::YieldInForLoop) {
if checker.is_rule_enabled(Rule::YieldInForLoop) {
pyupgrade::rules::yield_in_for_loop(checker, stmt_for);
}
if checker.enabled(Rule::UnnecessaryEnumerate) {
if checker.is_rule_enabled(Rule::UnnecessaryEnumerate) {
refurb::rules::unnecessary_enumerate(checker, stmt_for);
}
if checker.enabled(Rule::EnumerateForLoop) {
if checker.is_rule_enabled(Rule::EnumerateForLoop) {
flake8_simplify::rules::enumerate_for_loop(checker, stmt_for);
}
if checker.enabled(Rule::LoopIteratorMutation) {
if checker.is_rule_enabled(Rule::LoopIteratorMutation) {
flake8_bugbear::rules::loop_iterator_mutation(checker, stmt_for);
}
if checker.enabled(Rule::DictIndexMissingItems) {
if checker.is_rule_enabled(Rule::DictIndexMissingItems) {
pylint::rules::dict_index_missing_items(checker, stmt_for);
}
if checker.enabled(Rule::ManualDictComprehension) {
if checker.is_rule_enabled(Rule::ManualDictComprehension) {
perflint::rules::manual_dict_comprehension(checker, stmt_for);
}
if checker.enabled(Rule::ManualListComprehension) {
if checker.is_rule_enabled(Rule::ManualListComprehension) {
perflint::rules::manual_list_comprehension(checker, stmt_for);
}
}

View File

@@ -15,13 +15,13 @@ pub(crate) fn deferred_lambdas(checker: &mut Checker) {
unreachable!("Expected Expr::Lambda");
};
if checker.enabled(Rule::UnnecessaryLambda) {
if checker.is_rule_enabled(Rule::UnnecessaryLambda) {
pylint::rules::unnecessary_lambda(checker, lambda);
}
if checker.enabled(Rule::ReimplementedContainerBuiltin) {
if checker.is_rule_enabled(Rule::ReimplementedContainerBuiltin) {
flake8_pie::rules::reimplemented_container_builtin(checker, lambda);
}
if checker.enabled(Rule::BuiltinLambdaArgumentShadowing) {
if checker.is_rule_enabled(Rule::BuiltinLambdaArgumentShadowing) {
flake8_builtins::rules::builtin_lambda_argument_shadowing(checker, lambda);
}
}

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,
@@ -14,7 +9,7 @@ use crate::rules::{
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
pub(crate) fn deferred_scopes(checker: &Checker) {
if !checker.any_enabled(&[
if !checker.any_rule_enabled(&[
Rule::AsyncioDanglingTask,
Rule::BadStaticmethodArgument,
Rule::BuiltinAttributeShadowing,
@@ -58,7 +53,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
// used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only.
let enforce_typing_only_imports = !checker.source_type.is_stub()
&& checker.any_enabled(&[
&& checker.any_rule_enabled(&[
Rule::TypingOnlyFirstPartyImport,
Rule::TypingOnlyStandardLibraryImport,
Rule::TypingOnlyThirdPartyImport,
@@ -76,7 +71,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
flake8_type_checking::helpers::is_valid_runtime_import(
binding,
&checker.semantic,
&checker.settings.flake8_type_checking,
&checker.settings().flake8_type_checking,
)
})
.collect()
@@ -89,307 +84,66 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
for scope_id in checker.analyze.scopes.iter().rev().copied() {
let scope = &checker.semantic.scopes[scope_id];
if checker.enabled(Rule::UndefinedLocal) {
if checker.is_rule_enabled(Rule::UndefinedLocal) {
pyflakes::rules::undefined_local(checker, scope_id, scope);
}
if checker.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(),
);
}
}
}
if checker.is_rule_enabled(Rule::GlobalVariableNotAssigned) {
pylint::rules::global_variable_not_assigned(checker, scope);
}
if checker.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(),
);
}
}
if checker.is_rule_enabled(Rule::RedefinedArgumentFromLocal) {
pylint::rules::redefined_argument_from_local(checker, scope_id, scope);
}
if checker.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(),
);
}
}
if checker.is_rule_enabled(Rule::ImportShadowedByLoopVar) {
pyflakes::rules::import_shadowed_by_loop_var(checker, scope_id, scope);
}
if checker.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());
}
}
}
if checker.is_rule_enabled(Rule::RedefinedWhileUnused) {
pyflakes::rules::redefined_while_unused(checker, scope_id, scope);
}
if checker.source_type.is_stub()
|| matches!(scope.kind, ScopeKind::Module | ScopeKind::Function(_))
{
if checker.enabled(Rule::UnusedPrivateTypeVar) {
if checker.is_rule_enabled(Rule::UnusedPrivateTypeVar) {
flake8_pyi::rules::unused_private_type_var(checker, scope);
}
if checker.enabled(Rule::UnusedPrivateProtocol) {
if checker.is_rule_enabled(Rule::UnusedPrivateProtocol) {
flake8_pyi::rules::unused_private_protocol(checker, scope);
}
if checker.enabled(Rule::UnusedPrivateTypeAlias) {
if checker.is_rule_enabled(Rule::UnusedPrivateTypeAlias) {
flake8_pyi::rules::unused_private_type_alias(checker, scope);
}
if checker.enabled(Rule::UnusedPrivateTypedDict) {
if checker.is_rule_enabled(Rule::UnusedPrivateTypedDict) {
flake8_pyi::rules::unused_private_typed_dict(checker, scope);
}
}
if checker.enabled(Rule::AsyncioDanglingTask) {
if checker.is_rule_enabled(Rule::AsyncioDanglingTask) {
ruff::rules::asyncio_dangling_binding(scope, checker);
}
if let Some(class_def) = scope.kind.as_class() {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
if checker.is_rule_enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker, scope_id, scope, class_def,
);
}
if checker.enabled(Rule::FunctionCallInDataclassDefaultArgument) {
if checker.is_rule_enabled(Rule::FunctionCallInDataclassDefaultArgument) {
ruff::rules::function_call_in_dataclass_default(checker, class_def);
}
if checker.enabled(Rule::MutableClassDefault) {
if checker.is_rule_enabled(Rule::MutableClassDefault) {
ruff::rules::mutable_class_default(checker, class_def);
}
if checker.enabled(Rule::MutableDataclassDefault) {
if checker.is_rule_enabled(Rule::MutableDataclassDefault) {
ruff::rules::mutable_dataclass_default(checker, class_def);
}
}
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
if checker.any_enabled(&[Rule::UnusedVariable, Rule::UnusedUnpackedVariable])
if checker.any_rule_enabled(&[Rule::UnusedVariable, Rule::UnusedUnpackedVariable])
&& !(scope.uses_locals() && scope.kind.is_function())
{
let unused_bindings = scope
@@ -402,7 +156,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
&& binding.is_unused()
&& !binding.is_nonlocal()
&& !binding.is_global()
&& !checker.settings.dummy_variable_rgx.is_match(name)
&& !checker.settings().dummy_variable_rgx.is_match(name)
&& !matches!(
name,
"__tracebackhide__"
@@ -418,22 +172,22 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
});
for (unused_name, unused_binding) in unused_bindings {
if checker.enabled(Rule::UnusedVariable) {
if checker.is_rule_enabled(Rule::UnusedVariable) {
pyflakes::rules::unused_variable(checker, unused_name, unused_binding);
}
if checker.enabled(Rule::UnusedUnpackedVariable) {
if checker.is_rule_enabled(Rule::UnusedUnpackedVariable) {
ruff::rules::unused_unpacked_variable(checker, unused_name, unused_binding);
}
}
}
if checker.enabled(Rule::UnusedAnnotation) {
if checker.is_rule_enabled(Rule::UnusedAnnotation) {
pyflakes::rules::unused_annotation(checker, scope);
}
if !checker.source_type.is_stub() {
if checker.any_enabled(&[
if checker.any_rule_enabled(&[
Rule::UnusedClassMethodArgument,
Rule::UnusedFunctionArgument,
Rule::UnusedLambdaArgument,
@@ -447,7 +201,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if !checker.source_type.is_stub()
&& checker.enabled(Rule::RuntimeImportInTypeCheckingBlock)
&& checker.is_rule_enabled(Rule::RuntimeImportInTypeCheckingBlock)
{
flake8_type_checking::rules::runtime_import_in_type_checking_block(checker, scope);
}
@@ -467,37 +221,37 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
);
}
if checker.enabled(Rule::UnusedImport) {
if checker.is_rule_enabled(Rule::UnusedImport) {
pyflakes::rules::unused_import(checker, scope);
}
if checker.enabled(Rule::ImportPrivateName) {
if checker.is_rule_enabled(Rule::ImportPrivateName) {
pylint::rules::import_private_name(checker, scope);
}
}
if scope.kind.is_function() {
if checker.enabled(Rule::NoSelfUse) {
if checker.is_rule_enabled(Rule::NoSelfUse) {
pylint::rules::no_self_use(checker, scope_id, scope);
}
if checker.enabled(Rule::TooManyLocals) {
if checker.is_rule_enabled(Rule::TooManyLocals) {
pylint::rules::too_many_locals(checker, scope);
}
if checker.enabled(Rule::SingledispatchMethod) {
if checker.is_rule_enabled(Rule::SingledispatchMethod) {
pylint::rules::singledispatch_method(checker, scope);
}
if checker.enabled(Rule::SingledispatchmethodFunction) {
if checker.is_rule_enabled(Rule::SingledispatchmethodFunction) {
pylint::rules::singledispatchmethod_function(checker, scope);
}
if checker.enabled(Rule::BadStaticmethodArgument) {
if checker.is_rule_enabled(Rule::BadStaticmethodArgument) {
pylint::rules::bad_staticmethod_argument(checker, scope);
}
if checker.any_enabled(&[
if checker.any_rule_enabled(&[
Rule::InvalidFirstArgumentNameForClassMethod,
Rule::InvalidFirstArgumentNameForMethod,
]) {

View File

@@ -17,7 +17,7 @@ use crate::{docstrings, warn_user};
/// it is expected that all [`Definition`] nodes have been visited by the time, and that this
/// method will not recurse into any other nodes.
pub(crate) fn definitions(checker: &mut Checker) {
let enforce_annotations = checker.any_enabled(&[
let enforce_annotations = checker.any_rule_enabled(&[
Rule::AnyType,
Rule::MissingReturnTypeClassMethod,
Rule::MissingReturnTypePrivateFunction,
@@ -28,10 +28,11 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::MissingTypeFunctionArgument,
Rule::MissingTypeKwargs,
]);
let enforce_stubs = checker.source_type.is_stub() && checker.enabled(Rule::DocstringInStub);
let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable);
let enforce_dunder_method = checker.enabled(Rule::BadDunderMethodName);
let enforce_docstrings = checker.any_enabled(&[
let enforce_stubs =
checker.source_type.is_stub() && checker.is_rule_enabled(Rule::DocstringInStub);
let enforce_stubs_and_runtime = checker.is_rule_enabled(Rule::IterMethodReturnIterable);
let enforce_dunder_method = checker.is_rule_enabled(Rule::BadDunderMethodName);
let enforce_docstrings = checker.any_rule_enabled(&[
Rule::MissingBlankLineAfterLastSection,
Rule::MissingBlankLineAfterSummary,
Rule::BlankLineBeforeClass,
@@ -79,7 +80,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::UndocumentedPublicNestedClass,
Rule::UndocumentedPublicPackage,
]);
let enforce_pydoclint = checker.any_enabled(&[
let enforce_pydoclint = checker.any_rule_enabled(&[
Rule::DocstringMissingReturns,
Rule::DocstringExtraneousReturns,
Rule::DocstringMissingYields,
@@ -166,7 +167,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
if enforce_docstrings || enforce_pydoclint {
if pydocstyle::helpers::should_ignore_definition(
definition,
&checker.settings.pydocstyle,
&checker.settings().pydocstyle,
&checker.semantic,
) {
continue;
@@ -202,74 +203,76 @@ pub(crate) fn definitions(checker: &mut Checker) {
if !pydocstyle::rules::not_empty(checker, &docstring) {
continue;
}
if checker.enabled(Rule::UnnecessaryMultilineDocstring) {
if checker.is_rule_enabled(Rule::UnnecessaryMultilineDocstring) {
pydocstyle::rules::one_liner(checker, &docstring);
}
if checker.any_enabled(&[Rule::BlankLineAfterFunction, Rule::BlankLineBeforeFunction]) {
if checker
.any_rule_enabled(&[Rule::BlankLineAfterFunction, Rule::BlankLineBeforeFunction])
{
pydocstyle::rules::blank_before_after_function(checker, &docstring);
}
if checker.any_enabled(&[
if checker.any_rule_enabled(&[
Rule::BlankLineBeforeClass,
Rule::IncorrectBlankLineAfterClass,
Rule::IncorrectBlankLineBeforeClass,
]) {
pydocstyle::rules::blank_before_after_class(checker, &docstring);
}
if checker.enabled(Rule::MissingBlankLineAfterSummary) {
if checker.is_rule_enabled(Rule::MissingBlankLineAfterSummary) {
pydocstyle::rules::blank_after_summary(checker, &docstring);
}
if checker.any_enabled(&[
if checker.any_rule_enabled(&[
Rule::DocstringTabIndentation,
Rule::OverIndentation,
Rule::UnderIndentation,
]) {
pydocstyle::rules::indent(checker, &docstring);
}
if checker.enabled(Rule::NewLineAfterLastParagraph) {
if checker.is_rule_enabled(Rule::NewLineAfterLastParagraph) {
pydocstyle::rules::newline_after_last_paragraph(checker, &docstring);
}
if checker.enabled(Rule::SurroundingWhitespace) {
if checker.is_rule_enabled(Rule::SurroundingWhitespace) {
pydocstyle::rules::no_surrounding_whitespace(checker, &docstring);
}
if checker.any_enabled(&[
if checker.any_rule_enabled(&[
Rule::MultiLineSummaryFirstLine,
Rule::MultiLineSummarySecondLine,
]) {
pydocstyle::rules::multi_line_summary_start(checker, &docstring);
}
if checker.enabled(Rule::TripleSingleQuotes) {
if checker.is_rule_enabled(Rule::TripleSingleQuotes) {
pydocstyle::rules::triple_quotes(checker, &docstring);
}
if checker.enabled(Rule::EscapeSequenceInDocstring) {
if checker.is_rule_enabled(Rule::EscapeSequenceInDocstring) {
pydocstyle::rules::backslashes(checker, &docstring);
}
if checker.enabled(Rule::MissingTrailingPeriod) {
if checker.is_rule_enabled(Rule::MissingTrailingPeriod) {
pydocstyle::rules::ends_with_period(checker, &docstring);
}
if checker.enabled(Rule::NonImperativeMood) {
if checker.is_rule_enabled(Rule::NonImperativeMood) {
pydocstyle::rules::non_imperative_mood(
checker,
&docstring,
&checker.settings.pydocstyle,
&checker.settings().pydocstyle,
);
}
if checker.enabled(Rule::SignatureInDocstring) {
if checker.is_rule_enabled(Rule::SignatureInDocstring) {
pydocstyle::rules::no_signature(checker, &docstring);
}
if checker.enabled(Rule::FirstWordUncapitalized) {
if checker.is_rule_enabled(Rule::FirstWordUncapitalized) {
pydocstyle::rules::capitalized(checker, &docstring);
}
if checker.enabled(Rule::DocstringStartsWithThis) {
if checker.is_rule_enabled(Rule::DocstringStartsWithThis) {
pydocstyle::rules::starts_with_this(checker, &docstring);
}
if checker.enabled(Rule::MissingTerminalPunctuation) {
if checker.is_rule_enabled(Rule::MissingTerminalPunctuation) {
pydocstyle::rules::ends_with_punctuation(checker, &docstring);
}
if checker.enabled(Rule::OverloadWithDocstring) {
if checker.is_rule_enabled(Rule::OverloadWithDocstring) {
pydocstyle::rules::if_needed(checker, &docstring);
}
let enforce_sections = checker.any_enabled(&[
let enforce_sections = checker.any_rule_enabled(&[
Rule::MissingBlankLineAfterLastSection,
Rule::BlankLinesBetweenHeaderAndContent,
Rule::NonCapitalizedSectionName,
@@ -289,7 +292,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
if enforce_sections || enforce_pydoclint {
let section_contexts = pydocstyle::helpers::get_section_contexts(
&docstring,
checker.settings.pydocstyle.convention(),
checker.settings().pydocstyle.convention(),
);
if enforce_sections {
@@ -297,7 +300,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
checker,
&docstring,
&section_contexts,
checker.settings.pydocstyle.convention(),
checker.settings().pydocstyle.convention(),
);
}
@@ -307,7 +310,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
definition,
&docstring,
&section_contexts,
checker.settings.pydocstyle.convention(),
checker.settings().pydocstyle.convention(),
);
}
}

View File

@@ -17,17 +17,17 @@ pub(crate) fn except_handler(except_handler: &ExceptHandler, checker: &Checker)
range: _,
node_index: _,
}) => {
if checker.enabled(Rule::BareExcept) {
if checker.is_rule_enabled(Rule::BareExcept) {
pycodestyle::rules::bare_except(checker, type_.as_deref(), body, except_handler);
}
if checker.enabled(Rule::RaiseWithoutFromInsideExcept) {
if checker.is_rule_enabled(Rule::RaiseWithoutFromInsideExcept) {
flake8_bugbear::rules::raise_without_from_inside_except(
checker,
name.as_deref(),
body,
);
}
if checker.enabled(Rule::BlindExcept) {
if checker.is_rule_enabled(Rule::BlindExcept) {
flake8_blind_except::rules::blind_except(
checker,
type_.as_deref(),
@@ -35,42 +35,42 @@ pub(crate) fn except_handler(except_handler: &ExceptHandler, checker: &Checker)
body,
);
}
if checker.enabled(Rule::TryExceptPass) {
if checker.is_rule_enabled(Rule::TryExceptPass) {
flake8_bandit::rules::try_except_pass(
checker,
except_handler,
type_.as_deref(),
body,
checker.settings.flake8_bandit.check_typed_exception,
checker.settings().flake8_bandit.check_typed_exception,
);
}
if checker.enabled(Rule::TryExceptContinue) {
if checker.is_rule_enabled(Rule::TryExceptContinue) {
flake8_bandit::rules::try_except_continue(
checker,
except_handler,
type_.as_deref(),
body,
checker.settings.flake8_bandit.check_typed_exception,
checker.settings().flake8_bandit.check_typed_exception,
);
}
if checker.enabled(Rule::ExceptWithEmptyTuple) {
if checker.is_rule_enabled(Rule::ExceptWithEmptyTuple) {
flake8_bugbear::rules::except_with_empty_tuple(checker, except_handler);
}
if checker.enabled(Rule::ExceptWithNonExceptionClasses) {
if checker.is_rule_enabled(Rule::ExceptWithNonExceptionClasses) {
flake8_bugbear::rules::except_with_non_exception_classes(checker, except_handler);
}
if checker.enabled(Rule::BinaryOpException) {
if checker.is_rule_enabled(Rule::BinaryOpException) {
pylint::rules::binary_op_exception(checker, except_handler);
}
if let Some(name) = name {
if checker.enabled(Rule::AmbiguousVariableName) {
if checker.is_rule_enabled(Rule::AmbiguousVariableName) {
pycodestyle::rules::ambiguous_variable_name(
checker,
name.as_str(),
name.range(),
);
}
if checker.enabled(Rule::BuiltinVariableShadowing) {
if checker.is_rule_enabled(Rule::BuiltinVariableShadowing) {
flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,10 @@ use crate::rules::{flake8_bugbear, ruff};
/// Run lint rules over a module.
pub(crate) fn module(suite: &Suite, checker: &Checker) {
if checker.enabled(Rule::FStringDocstring) {
if checker.is_rule_enabled(Rule::FStringDocstring) {
flake8_bugbear::rules::f_string_docstring(checker, suite);
}
if checker.enabled(Rule::InvalidFormatterSuppressionComment) {
if checker.is_rule_enabled(Rule::InvalidFormatterSuppressionComment) {
ruff::rules::ignored_formatter_suppression_comment(checker, suite);
}
}

View File

@@ -7,14 +7,14 @@ use crate::rules::{flake8_builtins, pycodestyle};
/// Run lint rules over a [`Parameter`] syntax node.
pub(crate) fn parameter(parameter: &Parameter, checker: &Checker) {
if checker.enabled(Rule::AmbiguousVariableName) {
if checker.is_rule_enabled(Rule::AmbiguousVariableName) {
pycodestyle::rules::ambiguous_variable_name(
checker,
&parameter.name,
parameter.name.range(),
);
}
if checker.enabled(Rule::BuiltinArgumentShadowing) {
if checker.is_rule_enabled(Rule::BuiltinArgumentShadowing) {
flake8_builtins::rules::builtin_argument_shadowing(checker, parameter);
}
}

View File

@@ -6,17 +6,17 @@ use crate::rules::{flake8_bugbear, flake8_pyi, ruff};
/// Run lint rules over a [`Parameters`] syntax node.
pub(crate) fn parameters(parameters: &Parameters, checker: &Checker) {
if checker.enabled(Rule::FunctionCallInDefaultArgument) {
if checker.is_rule_enabled(Rule::FunctionCallInDefaultArgument) {
flake8_bugbear::rules::function_call_in_argument_default(checker, parameters);
}
if checker.settings.rules.enabled(Rule::ImplicitOptional) {
if checker.is_rule_enabled(Rule::ImplicitOptional) {
ruff::rules::implicit_optional(checker, parameters);
}
if checker.source_type.is_stub() {
if checker.enabled(Rule::TypedArgumentDefaultInStub) {
if checker.is_rule_enabled(Rule::TypedArgumentDefaultInStub) {
flake8_pyi::rules::typed_argument_simple_defaults(checker, parameters);
}
if checker.enabled(Rule::ArgumentDefaultInStub) {
if checker.is_rule_enabled(Rule::ArgumentDefaultInStub) {
flake8_pyi::rules::argument_simple_defaults(checker, parameters);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,37 +6,39 @@ use crate::rules::{flake8_bandit, flake8_pyi, flake8_quotes, pycodestyle, ruff};
/// Run lint rules over a [`StringLike`] syntax nodes.
pub(crate) fn string_like(string_like: StringLike, checker: &Checker) {
if checker.any_enabled(&[
if checker.any_rule_enabled(&[
Rule::AmbiguousUnicodeCharacterString,
Rule::AmbiguousUnicodeCharacterDocstring,
]) {
ruff::rules::ambiguous_unicode_character_string(checker, string_like);
}
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
if checker.is_rule_enabled(Rule::HardcodedBindAllInterfaces) {
flake8_bandit::rules::hardcoded_bind_all_interfaces(checker, string_like);
}
if checker.enabled(Rule::HardcodedTempFile) {
if checker.is_rule_enabled(Rule::HardcodedTempFile) {
flake8_bandit::rules::hardcoded_tmp_directory(checker, string_like);
}
if checker.source_type.is_stub() {
if checker.enabled(Rule::StringOrBytesTooLong) {
if checker.is_rule_enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, string_like);
}
}
if checker.any_enabled(&[
if checker.any_rule_enabled(&[
Rule::BadQuotesInlineString,
Rule::BadQuotesMultilineString,
Rule::BadQuotesDocstring,
]) {
flake8_quotes::rules::check_string_quotes(checker, string_like);
}
if checker.enabled(Rule::UnnecessaryEscapedQuote) {
if checker.is_rule_enabled(Rule::UnnecessaryEscapedQuote) {
flake8_quotes::rules::unnecessary_escaped_quote(checker, string_like);
}
if checker.enabled(Rule::AvoidableEscapedQuote) && checker.settings.flake8_quotes.avoid_escape {
if checker.is_rule_enabled(Rule::AvoidableEscapedQuote)
&& checker.settings().flake8_quotes.avoid_escape
{
flake8_quotes::rules::avoidable_escaped_quote(checker, string_like);
}
if checker.enabled(Rule::InvalidEscapeSequence) {
if checker.is_rule_enabled(Rule::InvalidEscapeSequence) {
pycodestyle::rules::invalid_escape_sequence(checker, string_like);
}
}

View File

@@ -7,10 +7,10 @@ use crate::rules::refurb;
/// Run lint rules over a suite of [`Stmt`] syntax nodes.
pub(crate) fn suite(suite: &[Stmt], checker: &Checker) {
if checker.enabled(Rule::UnnecessaryPlaceholder) {
if checker.is_rule_enabled(Rule::UnnecessaryPlaceholder) {
flake8_pie::rules::unnecessary_placeholder(checker, suite);
}
if checker.enabled(Rule::RepeatedGlobal) {
if checker.is_rule_enabled(Rule::RepeatedGlobal) {
refurb::rules::repeated_global(checker, suite);
}
}

View File

@@ -7,22 +7,22 @@ use crate::rules::pyflakes;
/// Run lint rules over all [`UnresolvedReference`] entities in the [`SemanticModel`].
pub(crate) fn unresolved_references(checker: &Checker) {
if !checker.any_enabled(&[Rule::UndefinedLocalWithImportStarUsage, Rule::UndefinedName]) {
if !checker.any_rule_enabled(&[Rule::UndefinedLocalWithImportStarUsage, Rule::UndefinedName]) {
return;
}
for reference in checker.semantic.unresolved_references() {
if reference.is_wildcard_import() {
if checker.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 {
if checker.enabled(Rule::UndefinedName) {
// 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;
@@ -37,8 +37,8 @@ use ruff_python_ast::str::Quote;
use ruff_python_ast::visitor::{Visitor, walk_except_handler, walk_pattern};
use ruff_python_ast::{
self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr,
ExprContext, InterpolatedStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters,
Pattern, PythonVersion, Stmt, Suite, UnaryOp,
ExprContext, ExprFString, ExprTString, InterpolatedStringElement, Keyword, MatchCase,
ModModule, Parameter, Parameters, Pattern, PythonVersion, Stmt, Suite, UnaryOp,
};
use ruff_python_ast::{PySourceType, helpers, str, visitor};
use ruff_python_codegen::{Generator, Stylist};
@@ -57,7 +57,7 @@ use ruff_python_semantic::{
};
use ruff_python_stdlib::builtins::{MAGIC_GLOBALS, python_builtins};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::{OneIndexed, SourceFile, SourceRow};
use ruff_source_file::{OneIndexed, SourceFile, SourceFileBuilder, SourceRow};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::annotation::AnnotationContext;
@@ -66,12 +66,13 @@ use crate::importer::{ImportRequest, Importer, ResolutionError};
use crate::noqa::NoqaMapping;
use crate::package::PackageRoot;
use crate::preview::is_undefined_export_in_dunder_init_enabled;
use crate::registry::{AsRule, Rule};
use crate::registry::Rule;
use crate::rules::pyflakes::rules::{
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
};
use crate::rules::pylint::rules::{AwaitOutsideAsync, LoadBeforeGlobalDeclaration};
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
use crate::settings::rule_table::RuleTable;
use crate::settings::{LinterSettings, TargetVersion, flags};
use crate::{Edit, OldDiagnostic, Violation};
use crate::{Locator, docstrings, noqa};
@@ -206,8 +207,6 @@ pub(crate) struct Checker<'a> {
/// The [`NoqaMapping`] for the current analysis (i.e., the mapping from line number to
/// suppression commented line number).
noqa_line_for: &'a NoqaMapping,
/// The [`LinterSettings`] for the current analysis, including the enabled rules.
pub(crate) settings: &'a LinterSettings,
/// The [`Locator`] for the current file, which enables extraction of source code from byte
/// offsets.
locator: &'a Locator<'a>,
@@ -259,13 +258,12 @@ impl<'a> Checker<'a> {
notebook_index: Option<&'a NotebookIndex>,
target_version: TargetVersion,
context: &'a LintContext<'a>,
) -> Checker<'a> {
) -> Self {
let semantic = SemanticModel::new(&settings.typing_modules, path, module);
Self {
parsed,
parsed_type_annotation: None,
parsed_annotations_cache: ParsedAnnotationsCache::new(parsed_annotations_arena),
settings,
noqa_line_for,
noqa,
path,
@@ -323,7 +321,8 @@ impl<'a> Checker<'a> {
/// Return the preferred quote for a generated `StringLiteral` node, given where we are in the
/// AST.
fn preferred_quote(&self) -> Quote {
self.f_string_quote_style().unwrap_or(self.stylist.quote())
self.interpolated_string_quote_style()
.unwrap_or(self.stylist.quote())
}
/// Return the default string flags a generated `StringLiteral` node should use, given where we
@@ -345,21 +344,27 @@ impl<'a> Checker<'a> {
ast::FStringFlags::empty().with_quote_style(self.preferred_quote())
}
/// Returns the appropriate quoting for f-string by reversing the one used outside of
/// the f-string.
/// Returns the appropriate quoting for interpolated strings by reversing the one used outside of
/// the interpolated string.
///
/// If the current expression in the context is not an f-string, returns ``None``.
pub(crate) fn f_string_quote_style(&self) -> Option<Quote> {
if !self.semantic.in_f_string() {
/// If the current expression in the context is not an interpolated string, returns ``None``.
pub(crate) fn interpolated_string_quote_style(&self) -> Option<Quote> {
if !self.semantic.in_interpolated_string() {
return None;
}
// Find the quote character used to start the containing f-string.
let ast::ExprFString { value, .. } = self
.semantic
// Find the quote character used to start the containing interpolated string.
self.semantic
.current_expressions()
.find_map(|expr| expr.as_f_string_expr())?;
Some(value.iter().next()?.quote_style().opposite())
.find_map(|expr| match expr {
Expr::FString(ExprFString { value, .. }) => {
Some(value.iter().next()?.quote_style().opposite())
}
Expr::TString(ExprTString { value, .. }) => {
Some(value.iter().next()?.quote_style().opposite())
}
_ => None,
})
}
/// Returns the [`SourceRow`] for the given offset.
@@ -383,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,
@@ -396,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,
@@ -456,6 +461,11 @@ impl<'a> Checker<'a> {
&self.semantic
}
/// The [`LinterSettings`] for the current analysis, including the enabled rules.
pub(crate) const fn settings(&self) -> &'a LinterSettings {
self.context.settings
}
/// The [`Path`] to the file under analysis.
pub(crate) const fn path(&self) -> &'a Path {
self.path
@@ -473,14 +483,14 @@ impl<'a> Checker<'a> {
/// Returns whether the given rule should be checked.
#[inline]
pub(crate) const fn enabled(&self, rule: Rule) -> bool {
self.settings.rules.enabled(rule)
pub(crate) const fn is_rule_enabled(&self, rule: Rule) -> bool {
self.context.is_rule_enabled(rule)
}
/// Returns whether any of the given rules should be checked.
#[inline]
pub(crate) const fn any_enabled(&self, rules: &[Rule]) -> bool {
self.settings.rules.any_enabled(rules)
pub(crate) const fn any_rule_enabled(&self, rules: &[Rule]) -> bool {
self.context.any_rule_enabled(rules)
}
/// Returns the [`IsolationLevel`] to isolate fixes for a given node.
@@ -568,7 +578,7 @@ impl<'a> Checker<'a> {
) -> Option<TypingImporter<'b, 'a>> {
let source_module = if self.target_version() >= version_added_to_typing {
"typing"
} else if !self.settings.typing_extensions {
} else if !self.settings().typing_extensions {
return None;
} else {
"typing_extensions"
@@ -615,16 +625,13 @@ impl SemanticSyntaxContext for Checker<'_> {
fn report_semantic_error(&self, error: SemanticSyntaxError) {
match error.kind {
SemanticSyntaxErrorKind::LateFutureImport => {
if self.settings.rules.enabled(Rule::LateFutureImport) {
// F404
if self.is_rule_enabled(Rule::LateFutureImport) {
self.report_diagnostic(LateFutureImport, error.range);
}
}
SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { name, start } => {
if self
.settings
.rules
.enabled(Rule::LoadBeforeGlobalDeclaration)
{
if self.is_rule_enabled(Rule::LoadBeforeGlobalDeclaration) {
self.report_diagnostic(
LoadBeforeGlobalDeclaration {
name,
@@ -635,17 +642,18 @@ impl SemanticSyntaxContext for Checker<'_> {
}
}
SemanticSyntaxErrorKind::YieldOutsideFunction(kind) => {
if self.settings.rules.enabled(Rule::YieldOutsideFunction) {
if self.is_rule_enabled(Rule::YieldOutsideFunction) {
self.report_diagnostic(YieldOutsideFunction::new(kind), error.range);
}
}
SemanticSyntaxErrorKind::ReturnOutsideFunction => {
if self.settings.rules.enabled(Rule::ReturnOutsideFunction) {
// F706
if self.is_rule_enabled(Rule::ReturnOutsideFunction) {
self.report_diagnostic(ReturnOutsideFunction, error.range);
}
}
SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(_) => {
if self.settings.rules.enabled(Rule::AwaitOutsideAsync) {
if self.is_rule_enabled(Rule::AwaitOutsideAsync) {
self.report_diagnostic(AwaitOutsideAsync, error.range);
}
}
@@ -1050,7 +1058,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
let annotation = AnnotationContext::from_function(
function_def,
&self.semantic,
self.settings,
self.settings(),
self.target_version(),
);
@@ -1252,7 +1260,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
}) => {
match AnnotationContext::from_model(
&self.semantic,
self.settings,
self.settings(),
self.target_version(),
) {
AnnotationContext::RuntimeRequired => {
@@ -1864,8 +1872,8 @@ impl<'a> Visitor<'a> for Checker<'a> {
match typing::match_annotated_subscript(
value,
&self.semantic,
self.settings.typing_modules.iter().map(String::as_str),
&self.settings.pyflakes.extend_generics,
self.settings().typing_modules.iter().map(String::as_str),
&self.settings().pyflakes.extend_generics,
) {
// Ex) Literal["Class"]
Some(typing::SubscriptKind::Literal) => {
@@ -2357,7 +2365,7 @@ impl<'a> Checker<'a> {
fn visit_cast_type_argument(&mut self, arg: &'a Expr) {
self.visit_type_definition(arg);
if !self.source_type.is_stub() && self.enabled(Rule::RuntimeCastValue) {
if !self.source_type.is_stub() && self.is_rule_enabled(Rule::RuntimeCastValue) {
flake8_type_checking::rules::runtime_cast_value(self, arg);
}
}
@@ -2472,6 +2480,7 @@ impl<'a> Checker<'a> {
fn bind_builtins(&mut self) {
let target_version = self.target_version();
let settings = self.settings();
let mut bind_builtin = |builtin| {
// Add the builtin to the scope.
let binding_id = self.semantic.push_builtin();
@@ -2485,7 +2494,7 @@ impl<'a> Checker<'a> {
for builtin in MAGIC_GLOBALS {
bind_builtin(builtin);
}
for builtin in &self.settings.builtins {
for builtin in &settings.builtins {
bind_builtin(builtin);
}
}
@@ -2759,12 +2768,12 @@ impl<'a> Checker<'a> {
if self.semantic.in_annotation()
&& self.semantic.in_typing_only_annotation()
{
if self.enabled(Rule::QuotedAnnotation) {
if self.is_rule_enabled(Rule::QuotedAnnotation) {
pyupgrade::rules::quoted_annotation(self, annotation, range);
}
}
if self.source_type.is_stub() {
if self.enabled(Rule::QuotedAnnotationInStub) {
if self.is_rule_enabled(Rule::QuotedAnnotationInStub) {
flake8_pyi::rules::quoted_annotation_in_stub(
self, annotation, range,
);
@@ -2786,7 +2795,9 @@ impl<'a> Checker<'a> {
self.visit_expr(parsed_expr);
if self.semantic.in_type_alias_value() {
// stub files are covered by PYI020
if !self.source_type.is_stub() && self.enabled(Rule::QuotedTypeAlias) {
if !self.source_type.is_stub()
&& self.is_rule_enabled(Rule::QuotedTypeAlias)
{
flake8_type_checking::rules::quoted_type_alias(
self,
parsed_expr,
@@ -2799,7 +2810,8 @@ impl<'a> Checker<'a> {
Err(parse_error) => {
self.semantic.restore(snapshot);
if self.enabled(Rule::ForwardAnnotationSyntaxError) {
// F722
if self.is_rule_enabled(Rule::ForwardAnnotationSyntaxError) {
self.report_type_diagnostic(
pyflakes::rules::ForwardAnnotationSyntaxError {
parse_error: parse_error.error.to_string(),
@@ -2946,7 +2958,8 @@ impl<'a> Checker<'a> {
self.semantic.flags -= SemanticModelFlags::DUNDER_ALL_DEFINITION;
} else {
if self.semantic.global_scope().uses_star_imports() {
if self.enabled(Rule::UndefinedLocalWithImportStarUsage) {
// F405
if self.is_rule_enabled(Rule::UndefinedLocalWithImportStarUsage) {
self.report_diagnostic(
pyflakes::rules::UndefinedLocalWithImportStarUsage {
name: name.to_string(),
@@ -2956,8 +2969,9 @@ impl<'a> Checker<'a> {
.set_parent(definition.start());
}
} else {
if self.enabled(Rule::UndefinedExport) {
if is_undefined_export_in_dunder_init_enabled(self.settings)
// F822
if self.is_rule_enabled(Rule::UndefinedExport) {
if is_undefined_export_in_dunder_init_enabled(self.settings())
|| !self.path.ends_with("__init__.py")
{
self.report_diagnostic(
@@ -3107,17 +3121,28 @@ pub(crate) fn check_ast(
/// a [`Violation`] to the contained [`OldDiagnostic`] collection on `Drop`.
pub(crate) struct LintContext<'a> {
diagnostics: RefCell<Vec<OldDiagnostic>>,
source_file: &'a SourceFile,
source_file: SourceFile,
rules: RuleTable,
settings: &'a LinterSettings,
}
impl<'a> LintContext<'a> {
/// Create a new collector with the given `source_file` and an empty collection of
/// `OldDiagnostic`s.
pub(crate) fn new(source_file: &'a SourceFile, settings: &'a LinterSettings) -> Self {
pub(crate) fn new(path: &Path, contents: &str, settings: &'a LinterSettings) -> Self {
let source_file =
SourceFileBuilder::new(path.to_string_lossy().as_ref(), contents).finish();
// Ignore diagnostics based on per-file-ignores.
let mut rules = settings.rules.clone();
for ignore in crate::fs::ignores_from_path(path, &settings.per_file_ignores) {
rules.disable(ignore);
}
Self {
diagnostics: RefCell::default(),
source_file,
rules,
settings,
}
}
@@ -3125,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,
@@ -3133,43 +3158,59 @@ impl<'a> LintContext<'a> {
) -> DiagnosticGuard<'chk, 'a> {
DiagnosticGuard {
context: self,
diagnostic: Some(OldDiagnostic::new(kind, range, self.source_file)),
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>> {
let diagnostic = OldDiagnostic::new(kind, range, self.source_file);
if self.settings.rules.enabled(diagnostic.rule()) {
let rule = T::rule();
if self.is_rule_enabled(rule) {
Some(DiagnosticGuard {
context: self,
diagnostic: Some(diagnostic),
diagnostic: Some(OldDiagnostic::new(kind, range, &self.source_file)),
rule,
})
} else {
None
}
}
pub(crate) fn into_diagnostics(self) -> Vec<OldDiagnostic> {
self.diagnostics.into_inner()
#[inline]
pub(crate) const fn is_rule_enabled(&self, rule: Rule) -> bool {
self.rules.enabled(rule)
}
pub(crate) fn is_empty(&self) -> bool {
self.diagnostics.borrow().is_empty()
#[inline]
pub(crate) const fn any_rule_enabled(&self, rules: &[Rule]) -> bool {
self.rules.any_enabled(rules)
}
#[inline]
pub(crate) fn iter_enabled_rules(&self) -> impl Iterator<Item = Rule> + '_ {
self.rules.iter_enabled()
}
#[inline]
pub(crate) fn into_parts(self) -> (Vec<OldDiagnostic>, SourceFile) {
(self.diagnostics.into_inner(), self.source_file)
}
#[inline]
pub(crate) fn as_mut_vec(&mut self) -> &mut Vec<OldDiagnostic> {
self.diagnostics.get_mut()
}
#[inline]
pub(crate) fn iter(&mut self) -> impl Iterator<Item = &OldDiagnostic> {
self.diagnostics.get_mut().iter()
}
@@ -3189,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<'_, '_> {
@@ -3201,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

@@ -23,7 +23,7 @@ pub(crate) fn check_file_path(
context: &LintContext,
) {
// flake8-no-pep420
if settings.rules.enabled(Rule::ImplicitNamespacePackage) {
if context.is_rule_enabled(Rule::ImplicitNamespacePackage) {
let allow_nested_roots = is_allow_nested_roots_enabled(settings);
implicit_namespace_package(
path,
@@ -38,12 +38,12 @@ pub(crate) fn check_file_path(
}
// pep8-naming
if settings.rules.enabled(Rule::InvalidModuleName) {
if context.is_rule_enabled(Rule::InvalidModuleName) {
invalid_module_name(path, package, &settings.pep8_naming.ignore_names, context);
}
// flake8-builtins
if settings.rules.enabled(Rule::StdlibModuleShadowing) {
if context.is_rule_enabled(Rule::StdlibModuleShadowing) {
stdlib_module_shadowing(path, settings, target_version, context);
}
}

View File

@@ -42,7 +42,7 @@ pub(crate) fn check_imports(
let blocks: Vec<&Block> = tracker.iter().collect();
// Enforce import rules.
if settings.rules.enabled(Rule::UnsortedImports) {
if context.is_rule_enabled(Rule::UnsortedImports) {
for block in &blocks {
if !block.imports.is_empty() {
isort::rules::organize_imports(
@@ -60,7 +60,7 @@ pub(crate) fn check_imports(
}
}
}
if settings.rules.enabled(Rule::MissingRequiredImport) {
if context.is_rule_enabled(Rule::MissingRequiredImport) {
isort::rules::add_required_imports(
parsed,
locator,

View File

@@ -47,49 +47,48 @@ pub(crate) fn check_logical_lines(
let mut prev_indent_level = None;
let indent_char = stylist.indentation().as_char();
let enforce_space_around_operator = settings.rules.any_enabled(&[
let enforce_space_around_operator = context.any_rule_enabled(&[
Rule::MultipleSpacesBeforeOperator,
Rule::MultipleSpacesAfterOperator,
Rule::TabBeforeOperator,
Rule::TabAfterOperator,
]);
let enforce_whitespace_around_named_parameter_equals = settings.rules.any_enabled(&[
let enforce_whitespace_around_named_parameter_equals = context.any_rule_enabled(&[
Rule::UnexpectedSpacesAroundKeywordParameterEquals,
Rule::MissingWhitespaceAroundParameterEquals,
]);
let enforce_missing_whitespace_around_operator = settings.rules.any_enabled(&[
let enforce_missing_whitespace_around_operator = context.any_rule_enabled(&[
Rule::MissingWhitespaceAroundOperator,
Rule::MissingWhitespaceAroundArithmeticOperator,
Rule::MissingWhitespaceAroundBitwiseOrShiftOperator,
Rule::MissingWhitespaceAroundModuloOperator,
]);
let enforce_missing_whitespace = settings.rules.enabled(Rule::MissingWhitespace);
let enforce_space_after_comma = settings
.rules
.any_enabled(&[Rule::MultipleSpacesAfterComma, Rule::TabAfterComma]);
let enforce_extraneous_whitespace = settings.rules.any_enabled(&[
let enforce_missing_whitespace = context.is_rule_enabled(Rule::MissingWhitespace);
let enforce_space_after_comma =
context.any_rule_enabled(&[Rule::MultipleSpacesAfterComma, Rule::TabAfterComma]);
let enforce_extraneous_whitespace = context.any_rule_enabled(&[
Rule::WhitespaceAfterOpenBracket,
Rule::WhitespaceBeforeCloseBracket,
Rule::WhitespaceBeforePunctuation,
]);
let enforce_whitespace_around_keywords = settings.rules.any_enabled(&[
let enforce_whitespace_around_keywords = context.any_rule_enabled(&[
Rule::MultipleSpacesAfterKeyword,
Rule::MultipleSpacesBeforeKeyword,
Rule::TabAfterKeyword,
Rule::TabBeforeKeyword,
]);
let enforce_missing_whitespace_after_keyword =
settings.rules.enabled(Rule::MissingWhitespaceAfterKeyword);
let enforce_whitespace_before_comment = settings.rules.any_enabled(&[
context.is_rule_enabled(Rule::MissingWhitespaceAfterKeyword);
let enforce_whitespace_before_comment = context.any_rule_enabled(&[
Rule::TooFewSpacesBeforeInlineComment,
Rule::NoSpaceAfterInlineComment,
Rule::NoSpaceAfterBlockComment,
Rule::MultipleLeadingHashesForBlockComment,
]);
let enforce_whitespace_before_parameters =
settings.rules.enabled(Rule::WhitespaceBeforeParameters);
let enforce_redundant_backslash = settings.rules.enabled(Rule::RedundantBackslash);
let enforce_indentation = settings.rules.any_enabled(&[
context.is_rule_enabled(Rule::WhitespaceBeforeParameters);
let enforce_redundant_backslash = context.is_rule_enabled(Rule::RedundantBackslash);
let enforce_indentation = context.any_rule_enabled(&[
Rule::IndentationWithInvalidMultiple,
Rule::NoIndentedBlock,
Rule::UnexpectedIndentation,

View File

@@ -12,7 +12,7 @@ use crate::fix::edits::delete_comment;
use crate::noqa::{
Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping,
};
use crate::registry::{AsRule, Rule, RuleSet};
use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target;
use crate::rules::pygrep_hooks;
use crate::rules::ruff;
@@ -22,7 +22,7 @@ use crate::{Edit, Fix, Locator};
use super::ast::LintContext;
#[expect(clippy::too_many_arguments)]
/// RUF100
pub(crate) fn check_noqa(
context: &mut LintContext,
path: &Path,
@@ -30,7 +30,6 @@ pub(crate) fn check_noqa(
comment_ranges: &CommentRanges,
noqa_line_for: &NoqaMapping,
analyze_directives: bool,
per_file_ignores: &RuleSet,
settings: &LinterSettings,
) -> Vec<usize> {
// Identify any codes that are globally exempted (within the current file).
@@ -47,14 +46,15 @@ pub(crate) fn check_noqa(
// Remove any ignored diagnostics.
'outer: for (index, diagnostic) in context.iter().enumerate() {
let rule = diagnostic.rule();
// Can't ignore syntax errors.
let Some(code) = diagnostic.noqa_code() else {
continue;
};
if matches!(rule, Rule::BlanketNOQA) {
if code == Rule::BlanketNOQA.noqa_code() {
continue;
}
let code = rule.noqa_code();
match &exemption {
FileExemption::All(_) => {
// If the file is exempted, ignore all diagnostics.
@@ -106,10 +106,9 @@ pub(crate) fn check_noqa(
// Enforce that the noqa directive was actually used (RUF100), unless RUF100 was itself
// suppressed.
if settings.rules.enabled(Rule::UnusedNOQA)
if context.is_rule_enabled(Rule::UnusedNOQA)
&& analyze_directives
&& !exemption.includes(Rule::UnusedNOQA)
&& !per_file_ignores.contains(Rule::UnusedNOQA)
{
let directives = noqa_directives
.lines()
@@ -148,7 +147,9 @@ pub(crate) fn check_noqa(
if seen_codes.insert(original_code) {
let is_code_used = if is_file_level {
context.iter().any(|diag| diag.rule().noqa_code() == code)
context
.iter()
.any(|diag| diag.noqa_code().is_some_and(|noqa| noqa == code))
} else {
matches.iter().any(|match_| *match_ == code)
} || settings
@@ -159,7 +160,7 @@ pub(crate) fn check_noqa(
if is_code_used {
valid_codes.push(original_code);
} else if let Ok(rule) = Rule::from_code(code) {
if settings.rules.enabled(rule) {
if context.is_rule_enabled(rule) {
unmatched_codes.push(original_code);
} else {
disabled_codes.push(original_code);
@@ -229,18 +230,12 @@ pub(crate) fn check_noqa(
}
}
if settings.rules.enabled(Rule::RedirectedNOQA)
&& !per_file_ignores.contains(Rule::RedirectedNOQA)
&& !exemption.includes(Rule::RedirectedNOQA)
{
if context.is_rule_enabled(Rule::RedirectedNOQA) && !exemption.includes(Rule::RedirectedNOQA) {
ruff::rules::redirected_noqa(context, &noqa_directives);
ruff::rules::redirected_file_noqa(context, &file_noqa_directives);
}
if settings.rules.enabled(Rule::BlanketNOQA)
&& !per_file_ignores.contains(Rule::BlanketNOQA)
&& !exemption.enumerates(Rule::BlanketNOQA)
{
if context.is_rule_enabled(Rule::BlanketNOQA) && !exemption.enumerates(Rule::BlanketNOQA) {
pygrep_hooks::rules::blanket_noqa(
context,
&noqa_directives,
@@ -249,8 +244,7 @@ pub(crate) fn check_noqa(
);
}
if settings.rules.enabled(Rule::InvalidRuleCode)
&& !per_file_ignores.contains(Rule::InvalidRuleCode)
if context.is_rule_enabled(Rule::InvalidRuleCode)
&& !exemption.enumerates(Rule::InvalidRuleCode)
{
ruff::rules::invalid_noqa_code(context, &noqa_directives, locator, &settings.external);

View File

@@ -26,15 +26,16 @@ pub(crate) fn check_physical_lines(
settings: &LinterSettings,
context: &LintContext,
) {
let enforce_doc_line_too_long = settings.rules.enabled(Rule::DocLineTooLong);
let enforce_line_too_long = settings.rules.enabled(Rule::LineTooLong);
let enforce_no_newline_at_end_of_file = settings.rules.enabled(Rule::MissingNewlineAtEndOfFile);
let enforce_mixed_spaces_and_tabs = settings.rules.enabled(Rule::MixedSpacesAndTabs);
let enforce_bidirectional_unicode = settings.rules.enabled(Rule::BidirectionalUnicode);
let enforce_trailing_whitespace = settings.rules.enabled(Rule::TrailingWhitespace);
let enforce_doc_line_too_long = context.is_rule_enabled(Rule::DocLineTooLong);
let enforce_line_too_long = context.is_rule_enabled(Rule::LineTooLong);
let enforce_no_newline_at_end_of_file =
context.is_rule_enabled(Rule::MissingNewlineAtEndOfFile);
let enforce_mixed_spaces_and_tabs = context.is_rule_enabled(Rule::MixedSpacesAndTabs);
let enforce_bidirectional_unicode = context.is_rule_enabled(Rule::BidirectionalUnicode);
let enforce_trailing_whitespace = context.is_rule_enabled(Rule::TrailingWhitespace);
let enforce_blank_line_contains_whitespace =
settings.rules.enabled(Rule::BlankLineWithWhitespace);
let enforce_copyright_notice = settings.rules.enabled(Rule::MissingCopyrightNotice);
context.is_rule_enabled(Rule::BlankLineWithWhitespace);
let enforce_copyright_notice = context.is_rule_enabled(Rule::MissingCopyrightNotice);
let mut doc_lines_iter = doc_lines.iter().peekable();
let comment_ranges = indexer.comment_ranges();
@@ -62,10 +63,10 @@ pub(crate) fn check_physical_lines(
}
if enforce_trailing_whitespace || enforce_blank_line_contains_whitespace {
trailing_whitespace(&line, locator, indexer, settings, context);
trailing_whitespace(&line, locator, indexer, context);
}
if settings.rules.enabled(Rule::IndentedFormFeed) {
if context.is_rule_enabled(Rule::IndentedFormFeed) {
indented_form_feed(&line, context);
}
}
@@ -81,10 +82,11 @@ pub(crate) fn check_physical_lines(
#[cfg(test)]
mod tests {
use std::path::Path;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::parse_module;
use ruff_source_file::SourceFileBuilder;
use crate::Locator;
use crate::checkers::ast::LintContext;
@@ -104,7 +106,6 @@ mod tests {
let stylist = Stylist::from_tokens(parsed.tokens(), locator.contents());
let check_with_max_line_length = |line_length: LineLength| {
let source_file = SourceFileBuilder::new("<filename>", line).finish();
let settings = LinterSettings {
pycodestyle: pycodestyle::settings::Settings {
max_line_length: line_length,
@@ -112,9 +113,9 @@ mod tests {
},
..LinterSettings::for_rule(Rule::LineTooLong)
};
let diagnostics = LintContext::new(&source_file, &settings);
let diagnostics = LintContext::new(Path::new("<filename>"), line, &settings);
check_physical_lines(&locator, &stylist, &indexer, &[], &settings, &diagnostics);
diagnostics.into_diagnostics()
diagnostics.into_parts().0
};
let line_length = LineLength::try_from(8).unwrap();
assert_eq!(check_with_max_line_length(line_length), vec![]);

View File

@@ -34,7 +34,7 @@ pub(crate) fn check_tokens(
) {
let comment_ranges = indexer.comment_ranges();
if settings.rules.any_enabled(&[
if context.any_rule_enabled(&[
Rule::BlankLineBetweenMethods,
Rule::BlankLinesTopLevel,
Rule::TooManyBlankLines,
@@ -53,36 +53,33 @@ pub(crate) fn check_tokens(
.check_lines(tokens);
}
if settings.rules.enabled(Rule::BlanketTypeIgnore) {
if context.is_rule_enabled(Rule::BlanketTypeIgnore) {
pygrep_hooks::rules::blanket_type_ignore(context, comment_ranges, locator);
}
if settings.rules.enabled(Rule::EmptyComment) {
if context.is_rule_enabled(Rule::EmptyComment) {
pylint::rules::empty_comments(context, comment_ranges, locator);
}
if settings
.rules
.enabled(Rule::AmbiguousUnicodeCharacterComment)
{
if context.is_rule_enabled(Rule::AmbiguousUnicodeCharacterComment) {
for range in comment_ranges {
ruff::rules::ambiguous_unicode_character_comment(context, locator, range, settings);
}
}
if settings.rules.enabled(Rule::CommentedOutCode) {
if context.is_rule_enabled(Rule::CommentedOutCode) {
eradicate::rules::commented_out_code(context, locator, comment_ranges, settings);
}
if settings.rules.enabled(Rule::UTF8EncodingDeclaration) {
if context.is_rule_enabled(Rule::UTF8EncodingDeclaration) {
pyupgrade::rules::unnecessary_coding_comment(context, locator, comment_ranges);
}
if settings.rules.enabled(Rule::TabIndentation) {
if context.is_rule_enabled(Rule::TabIndentation) {
pycodestyle::rules::tab_indentation(context, locator, indexer);
}
if settings.rules.any_enabled(&[
if context.any_rule_enabled(&[
Rule::InvalidCharacterBackspace,
Rule::InvalidCharacterSub,
Rule::InvalidCharacterEsc,
@@ -94,7 +91,7 @@ pub(crate) fn check_tokens(
}
}
if settings.rules.any_enabled(&[
if context.any_rule_enabled(&[
Rule::MultipleStatementsOnOneLineColon,
Rule::MultipleStatementsOnOneLineSemicolon,
Rule::UselessSemicolon,
@@ -109,14 +106,14 @@ pub(crate) fn check_tokens(
);
}
if settings.rules.any_enabled(&[
if context.any_rule_enabled(&[
Rule::SingleLineImplicitStringConcatenation,
Rule::MultiLineImplicitStringConcatenation,
]) {
flake8_implicit_str_concat::rules::implicit(context, tokens, locator, indexer, settings);
}
if settings.rules.any_enabled(&[
if context.any_rule_enabled(&[
Rule::MissingTrailingComma,
Rule::TrailingCommaOnBareTuple,
Rule::ProhibitedTrailingComma,
@@ -124,25 +121,25 @@ pub(crate) fn check_tokens(
flake8_commas::rules::trailing_commas(context, tokens, locator, indexer);
}
if settings.rules.enabled(Rule::ExtraneousParentheses) {
if context.is_rule_enabled(Rule::ExtraneousParentheses) {
pyupgrade::rules::extraneous_parentheses(context, tokens, locator);
}
if source_type.is_stub() && settings.rules.enabled(Rule::TypeCommentInStub) {
if source_type.is_stub() && context.is_rule_enabled(Rule::TypeCommentInStub) {
flake8_pyi::rules::type_comment_in_stub(context, locator, comment_ranges);
}
if settings.rules.any_enabled(&[
if context.any_rule_enabled(&[
Rule::ShebangNotExecutable,
Rule::ShebangMissingExecutableFile,
Rule::ShebangLeadingWhitespace,
Rule::ShebangNotFirstLine,
Rule::ShebangMissingPython,
]) {
flake8_executable::rules::from_tokens(context, path, locator, comment_ranges, settings);
flake8_executable::rules::from_tokens(context, path, locator, comment_ranges);
}
if settings.rules.any_enabled(&[
if context.any_rule_enabled(&[
Rule::InvalidTodoTag,
Rule::MissingTodoAuthor,
Rule::MissingTodoLink,
@@ -167,7 +164,7 @@ pub(crate) fn check_tokens(
flake8_fixme::rules::todos(context, &todo_comments);
}
if settings.rules.enabled(Rule::TooManyNewlinesAtEndOfFile) {
if context.is_rule_enabled(Rule::TooManyNewlinesAtEndOfFile) {
pycodestyle::rules::too_many_newlines_at_end_of_file(context, tokens, cell_offsets);
}
}

View File

@@ -1028,6 +1028,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable),
(Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection),
(Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises),
(Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AccessAnnotationsFromClassDict),
(Ruff, "064") => (RuleGroup::Preview, rules::ruff::rules::NonOctalPermissions),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
(Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode),

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)

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