Compare commits

..

37 Commits

Author SHA1 Message Date
Charlie Marsh
5120b36b67 Consider alternate split points for line-too-long exemptions 2023-11-30 22:53:50 -05:00
Charlie Marsh
69dfe0a207 Fix doc formatting for zero-sleep-call (#8937) 2023-11-30 22:34:09 -05:00
Charlie Marsh
46a174a22e Use full arguments range for zero-sleep-call (#8936) 2023-12-01 03:09:18 +00:00
Charlie Marsh
912c39ce2a Add support for @functools.singledispatch (#8934)
## Summary

When a function uses `@functools.singledispatch`, we need to treat the
first argument of any implementations as runtime-required.

Closes https://github.com/astral-sh/ruff/issues/6849.
2023-12-01 03:04:58 +00:00
Charlie Marsh
b2638c62a5 Update formatter fixtures (#8935)
I merged a branch that wasn't up-to-date, which left us with test
failures on `main`.
2023-12-01 02:57:05 +00:00
Charlie Marsh
eaa310429f Insert trailing comma when function breaks with single argument (#8921)
## Summary

Given:

```python
def _example_function_xxxxxxx(
    variable: Optional[List[str]]
) -> List[example.ExampleConfig]:
    pass
```

We should be inserting a trailing comma after the argument (as long as
it's a single-argument function). This was an inconsistency with Black,
but also led to some internal inconsistencies, whereby we added the
comma if the argument contained a trailing end-of-line comment, but not
otherwise.

Closes https://github.com/astral-sh/ruff/issues/8912.

## Test Plan

`cargo test`

Before:

| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99963 | 10596 | 146 |
| poetry | 0.99925 | 317 | 12 |
| transformers | 0.99967 | 2657 | 322 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99977 | 654 | 13 |
| zulip | 0.99970 | 1459 | 21 |

After:

| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99955 | 10596 | 213 |
| poetry | 0.99917 | 317 | 13 |
| transformers | 0.99967 | 2657 | 324 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99976 | 654 | 14 |
| zulip | 0.99957 | 1459 | 36 |
2023-11-30 21:49:28 -05:00
Charlie Marsh
019d9aebe9 Implement multiline dictionary and list hugging for preview style (#8293)
## Summary

This PR implement's Black's new single-argument hugging for lists, sets,
and dictionaries under preview style.

For example, this:

```python
foo(
    [
        1,
        2,
        3,
    ]
)
```

Would instead now be formatted as:

```python
foo([
    1,
    2,
    3,
])
```

A couple notes:

- This doesn't apply when the argument has a magic trailing comma.
- This _does_ apply when the argument is starred or double-starred.
- We don't apply this when there are comments before or after the
argument, though Black does in some cases (and moves the comments
outside the call parentheses).

It doesn't say it in the originating PR
(https://github.com/psf/black/pull/3964), but I think this also applies
to parenthesized expressions? At least, it does in my testing of preview
vs. stable, though it's possible that behavior predated the linked PR.

See: #8279.

## Test Plan

Before:

| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99963 | 10596 | 146 |
| poetry | 0.99925 | 317 | 12 |
| transformers | 0.99967 | 2657 | 322 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99977 | 654 | 13 |
| zulip | 0.99970 | 1459 | 21 |

After:

| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99963 | 10596 | 146 |
| poetry | 0.96215 | 317 | 34 |
| transformers | 0.99967 | 2657 | 322 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99977 | 654 | 13 |
| zulip | 0.99970 | 1459 | 21 |
2023-11-30 21:11:14 -05:00
Dhruv Manilawala
f06c5dc896 Use correct range for TRIO115 fix (#8933)
## Summary

This PR fixes the bug where the autofix for `TRIO115` was taking the
entire arguments range for the fix which included the parenthesis as
well. This means that the fix would remove the arguments and the
parenthesis. The fix is to use the correct range.

fixes: #8713 

## Test Plan

Update existing snapshots :)
2023-12-01 01:42:46 +00:00
Charlie Marsh
c1dc4a60be Apply some minor changes to unnecessary-list-index-lookup (#8932)
## Summary

I was late in reviewing this but found a few things I wanted to tweak.
No functional changes.
2023-12-01 00:53:26 +00:00
Steve C
70febb1862 [pylint] - add unnecessary-list-index-lookup (PLR1736) + autofix (#7999)
## Summary

Add
[R1736](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/unnecessary-list-index-lookup.html)
along with the autofix

See #970 

## Test Plan

`cargo test` and manually

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
2023-11-30 17:45:12 -06:00
Steve C
4212b41796 [pylint] - implement R0202 and R0203 with autofixes (#8335)
## Summary

Implements
[`no-classmethod-decorator`/`R0202`](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/no-classmethod-decorator.html)
and
[`no-staticmethod-decorator`/`R0203`](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/no-staticmethod-decorator.html)
with autofixes.

They're similar enough that all code is reusable for both.

See: #970 

## Test Plan

`cargo test`
2023-11-30 16:18:09 -06:00
Steve C
bbad4b4c93 Add autofix for PYI030 (#7934)
## Summary

Part 2 of implementing the reverted autofix for `PYI030`

Also handles `typing.Union` and `typing_extensions.Literal` etc, uses
the first subscript name it finds for each offensive line.

## Test Plan

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

`cargo test` and manually

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
2023-11-30 22:16:57 +00:00
Steve C
3ee1ec70cc Fix up some types in the ecosystem code (#8898)
## Summary

Fixes up the type annotations to make type analyzers a little happier 😄 

## Test Plan

N/A
2023-11-30 16:02:20 -06:00
Charlie Marsh
ee5d95f751 Remove duplicate imports from os-stat documentation (#8930)
Closes https://github.com/astral-sh/ruff/issues/8799.
2023-11-30 20:13:29 +00:00
Charlie Marsh
d674e7946d Ignore underlines when determining docstring logical lines (#8929) 2023-11-30 14:27:04 -05:00
Charlie Marsh
20782ab02c Support type alias statements in simple statement positions (#8916)
<!--
Thank you for contributing to Ruff! 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?
- Does this pull request include references to any relevant issues?
-->

## Summary

Our `SoftKeywordTokenizer` only respected soft keywords in compound
statement positions -- for example, at the start of a logical line:

```python
type X = int
```

However, type aliases can also appear in simple statement positions,
like:

```python
class Class: type X = int
```

(Note that `match` and `case` are _not_ valid keywords in such
positions.)

This PR upgrades the tokenizer to track both kinds of valid positions.

Closes https://github.com/astral-sh/ruff/issues/8900.
Closes https://github.com/astral-sh/ruff/issues/8899.

## Test Plan

`cargo test`
2023-11-30 19:15:19 +00:00
Charlie Marsh
073eddb1d9 Use Python version to determine typing rewrite safety (#8919)
## Summary

These rewrites are only (potentially) unsafe on Python versions that
predate their introduction into the standard library and grammar, so it
seems correct to mark them as safe on those later versions.
2023-11-29 22:22:04 -05:00
Charlie Marsh
e8da95d09c Document fix safety for flake8-comprehensions and some pyupgrade rules (#8918)
See: https://github.com/astral-sh/ruff/issues/7993.
2023-11-29 20:51:23 -05:00
Charlie Marsh
c324cb6202 [pep8-naming] Allow Django model loads in non-lowercase-variable-in-function (N806) (#8917)
## Summary

Allows assignments of the form, e.g., `Attachment =
apps.get_model("zerver", "Attachment")`, for better compatibility with
Django.

Closes https://github.com/astral-sh/ruff/issues/7675.

## Test Plan

`cargo test`
2023-11-29 20:43:40 -05:00
Charlie Marsh
774c77adae Avoid off-by-one error in with-item named expressions (#8915)
## Summary

Given `with (a := b): pass`, we truncate the `WithItem` range by one on
both sides such that the parentheses are part of the statement, rather
than the item. However, for `with (a := b) as x: pass`, we want to avoid
this trick.

Closes https://github.com/astral-sh/ruff/issues/8913.
2023-11-30 00:11:04 +00:00
Micha Reiser
fd70cd789f Update Black tests (#8901) 2023-11-30 00:09:55 +00:00
Andrey
08f3110f1e Optimize workflow run (#8225) 2023-11-30 00:09:33 +00:00
Micha Reiser
2314b9aaca Add benchmark for running all rules including preview rules (#8865) 2023-11-29 04:26:25 +00:00
Micha Reiser
cddc696896 Stop at the first resolved parent configuration (#8864) 2023-11-29 04:21:07 +00:00
Charlie Marsh
6435e4e4aa Enable auto-return-type involving Optional and Union annotations (#8885)
## Summary

Previously, this was only supported for Python 3.10 and later, since we
always use the PEP 604-style unions.
2023-11-28 18:35:55 -08:00
Dhruv Manilawala
ec7456bac0 Rename as_str to to_str (#8886)
This PR renames the method on `StringLiteralValue` from `as_str` to
`to_str`. The main motivation is to follow the naming convention as
described in the [Rust API
Guidelines](https://rust-lang.github.io/api-guidelines/naming.html#ad-hoc-conversions-follow-as_-to_-into_-conventions-c-conv).
This method can perform a string allocation in case the string is
implicitly concatenated.
2023-11-28 18:50:42 -06:00
Dhruv Manilawala
b28556d739 Update E402 to work at cell level for notebooks (#8872)
## Summary

This PR updates the `E402` rule to work at cell level for Jupyter
notebooks. This is enabled only in preview to gather feedback.

The implementation basically resets the import boundary flag on the
semantic model when we encounter the first statement in a cell.

Another potential solution is to introduce `E403` rule that is
specifically for notebooks that works at cell level while `E402` will be
disabled for notebooks.

## Test Plan

Add a notebook with imports in multiple cells and verify that the rule
works as expected.

resolves: #8669
2023-11-29 00:32:35 +00:00
Andrew Gallant
4957d94beb ruff_python_formatter: small cleanups in doctest formatting (#8871)
This PR contains a few small clean-ups that are responses to
@MichaReiser's review of my #8811 PR.
2023-11-28 18:43:07 -05:00
Charlie Marsh
5d554edace Allow booleans in @override methods (#8882)
Closes #8867.
2023-11-28 13:42:31 -08:00
Charlie Marsh
412688826c Avoid filtering out un-representable types in return annotation (#8881)
## Summary

Given `Union[Dict, None]` (in our internal representation), we were
filtering out `Dict` since we treat it as un-representable (i.e., we
can't convert it to an expression), returning just `None` as the type
annotation. We should require that all members of the union are
representable.

Closes https://github.com/astral-sh/ruff/issues/8879.
2023-11-28 21:10:42 +00:00
Dhruv Manilawala
47d80f29a7 Lexer start of line is false only for Mode::Expression (#8880)
## Summary

This PR fixes the bug in the lexer where the `Mode::Ipython` wasn't
being considered when initializing the soft keyword transformer which
wraps the lexer. This means that if the source code starts with either
`match` or `type` keyword, then the keywords were being considered as
name tokens instead. For example,

```python
match foo:
    case bar:
        pass
```

This would transform the `match` keyword into an identifier if the mode
is `Ipython`.

The fix is to reverse the condition in the soft keyword initializer so
that any new modes are by default considered as the lexer being at start
of line.

## Test Plan

Add a new test case for `Mode::Ipython` and verify the snapshot.

fixes: #8870
2023-11-28 20:38:25 +00:00
Dhruv Manilawala
9dee1883ce Update ruff-dev to use SourceKind (#8878)
Just a small quality of life improvement to be able to pass in the
Jupyter Notebook to `ruff-dev` CLI.
2023-11-28 14:27:35 -06:00
Andrew Gallant
f585e3e2dc remove several uses of unsafe (#8600)
This PR removes several uses of `unsafe`. I generally limited myself to
low hanging fruit that I could see. There are still a few remaining uses
of `unsafe` that looked a bit more difficult to remove (if possible at
all). But this gets rid of a good chunk of them.

I put each `unsafe` removal into its own commit with a justification for
why I did it. So I would encourage reviewing this PR commit-by-commit.
That way, we can legislate them independently. It's no problem to drop a
commit if we feel the `unsafe` should stay in that case.
2023-11-28 09:50:03 -05:00
Joffrey Bluthé
578ddf1bb1 [isort] Add support for length-sort settings (#8841)
## Summary

Closes #1567.

Add both `length-sort` and `length-sort-straight` settings for isort.

Here are a few notable points:
- The length is determined using the
[`unicode_width`](https://crates.io/crates/unicode-width) crate, i.e. we
are talking about displayed length (this is explicitly mentioned in the
description of the setting)
- The dots are taken into account in the length to be compatible with
the original isort
- I had to reorder a few fields of the module key struct for it all to
make sense (notably the `force_to_top` field is now the first one)

## Test Plan

I added tests for the following cases:
- Basic tests for length-sort with ASCII characters only
- Tests with non-ASCII characters
- Tests with relative imports
- Tests for length-sort-straight
2023-11-28 06:00:37 +00:00
Charlie Marsh
ed14fd9163 [pydocstyle] Avoid non-character breaks in over-indentation (D208) (#8866)
Closes https://github.com/astral-sh/ruff/issues/8844.
2023-11-27 21:47:35 -08:00
Tom Kuson
60eb11fa50 [refurb] Implement redundant-log-base (FURB163) (#8842)
## Summary

Implement
[`simplify-math-log`](https://github.com/dosisod/refurb/blob/master/refurb/checks/math/simplify_log.py)
as `redundant-log-base` (`FURB163`).

Auto-fixes

```python
import math

math.log(2, 2)
```

to

```python
import math

math.log2(2)
```

Related to #1348.

## Test Plan

`cargo test`
2023-11-27 23:57:00 +00:00
Andrew Gallant
33caa2ab1c ruff_python_formatter: move docstring handling to a submodule (#8861)
This turns `string` into a parent module with a `docstring` sub-module.
I arranged things this way because there are parts of the `string`
module that the `docstring` module wants to know about (such as a
`NormalizedString`). The alternative I think would be to make
`docstring` a sibling module and expose more of `string`'s internals.

I think I overall like this change because it gives docstring handling a
bit more room to breath. It has grown quite a bit with the addition of
code snippet formatting.

[This was suggested by
@charliermarsh.](https://github.com/astral-sh/ruff/pull/8811#discussion_r1401169531)
2023-11-27 13:32:26 -05:00
627 changed files with 28191 additions and 3063 deletions

View File

@@ -23,8 +23,13 @@ jobs:
name: "Determine changes"
runs-on: ubuntu-latest
outputs:
# Flag that is raised when any code that affects linter is changed
linter: ${{ steps.changed.outputs.linter_any_changed }}
# Flag that is raised when any code that affects formatter is changed
formatter: ${{ steps.changed.outputs.formatter_any_changed }}
# Flag that is raised when any code is changed
# This is superset of the linter and formatter
code: ${{ steps.changed.outputs.code_any_changed }}
steps:
- uses: actions/checkout@v4
with:
@@ -62,6 +67,12 @@ jobs:
- python/**
- .github/workflows/ci.yaml
code:
- "*/**"
- "!**/*.md"
- "!docs/**"
- "!assets/**"
cargo-fmt:
name: "cargo fmt"
runs-on: ubuntu-latest
@@ -74,6 +85,8 @@ jobs:
cargo-clippy:
name: "cargo clippy"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
@@ -88,6 +101,8 @@ jobs:
cargo-test-linux:
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
name: "cargo test (linux)"
steps:
- uses: actions/checkout@v4
@@ -112,6 +127,8 @@ jobs:
cargo-test-windows:
runs-on: windows-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
name: "cargo test (windows)"
steps:
- uses: actions/checkout@v4
@@ -129,6 +146,8 @@ jobs:
cargo-test-wasm:
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
name: "cargo test (wasm)"
steps:
- uses: actions/checkout@v4
@@ -148,6 +167,8 @@ jobs:
cargo-fuzz:
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
name: "cargo fuzz"
steps:
- uses: actions/checkout@v4
@@ -165,6 +186,8 @@ jobs:
scripts:
name: "test scripts"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
@@ -188,8 +211,7 @@ jobs:
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
# Ecosystem check needs linter and/or formatter changes.
if: github.event_name == 'pull_request' && ${{
needs.determine_changes.outputs.linter == 'true' ||
needs.determine_changes.outputs.formatter == 'true'
needs.determine_changes.outputs.code == 'true'
}}
steps:
- uses: actions/checkout@v4
@@ -298,6 +320,8 @@ jobs:
cargo-udeps:
name: "cargo udeps"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: "Install nightly Rust toolchain"
@@ -417,7 +441,10 @@ jobs:
check-ruff-lsp:
name: "test ruff-lsp"
runs-on: ubuntu-latest
needs: cargo-test-linux
needs:
- cargo-test-linux
- determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
steps:
- uses: extractions/setup-just@v1
env:
@@ -455,6 +482,8 @@ jobs:
benchmarks:
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
steps:
- name: "Checkout Branch"
uses: actions/checkout@v4

View File

@@ -3,7 +3,9 @@ use ruff_benchmark::criterion::{
};
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_linter::linter::lint_only;
use ruff_linter::rule_selector::PreviewOptions;
use ruff_linter::settings::rule_table::RuleTable;
use ruff_linter::settings::types::PreviewMode;
use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::source_kind::SourceKind;
use ruff_linter::{registry::Rule, RuleSelector};
@@ -78,12 +80,21 @@ fn benchmark_default_rules(criterion: &mut Criterion) {
benchmark_linter(group, &LinterSettings::default());
}
fn benchmark_all_rules(criterion: &mut Criterion) {
let mut rules: RuleTable = RuleSelector::All.all_rules().collect();
// Disable IO based rules because it is a source of flakiness
/// Disables IO based rules because they are a source of flakiness
fn disable_io_rules(rules: &mut RuleTable) {
rules.disable(Rule::ShebangMissingExecutableFile);
rules.disable(Rule::ShebangNotExecutable);
}
fn benchmark_all_rules(criterion: &mut Criterion) {
let mut rules: RuleTable = RuleSelector::All
.rules(&PreviewOptions {
mode: PreviewMode::Disabled,
require_explicit: false,
})
.collect();
disable_io_rules(&mut rules);
let settings = LinterSettings {
rules,
@@ -94,6 +105,22 @@ fn benchmark_all_rules(criterion: &mut Criterion) {
benchmark_linter(group, &settings);
}
fn benchmark_preview_rules(criterion: &mut Criterion) {
let mut rules: RuleTable = RuleSelector::All.all_rules().collect();
disable_io_rules(&mut rules);
let settings = LinterSettings {
rules,
preview: PreviewMode::Enabled,
..LinterSettings::default()
};
let group = criterion.benchmark_group("linter/all-with-preview-rules");
benchmark_linter(group, &settings);
}
criterion_group!(default_rules, benchmark_default_rules);
criterion_group!(all_rules, benchmark_all_rules);
criterion_main!(default_rules, all_rules);
criterion_group!(preview_rules, benchmark_preview_rules);
criterion_main!(default_rules, all_rules, preview_rules);

View File

@@ -396,3 +396,43 @@ if __name__ == "__main__":
"###);
Ok(())
}
/// Regression test for [#8858](https://github.com/astral-sh/ruff/issues/8858)
#[test]
fn parent_configuration_override() -> Result<()> {
let tempdir = TempDir::new()?;
let root_ruff = tempdir.path().join("ruff.toml");
fs::write(
root_ruff,
r#"
[lint]
select = ["ALL"]
"#,
)?;
let sub_dir = tempdir.path().join("subdirectory");
fs::create_dir(&sub_dir)?;
let subdirectory_ruff = sub_dir.join("ruff.toml");
fs::write(
subdirectory_ruff,
r#"
[lint]
ignore = ["D203", "D212"]
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(sub_dir)
.arg("check")
.args(STDIN_BASE_OPTIONS)
, @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: No Python files found under the given path(s)
"###);
Ok(())
}

View File

@@ -1,30 +1,34 @@
//! Print the AST for a given Python file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use ruff_python_parser::{parse, Mode};
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::PySourceType;
use ruff_python_parser::{parse, AsMode};
#[derive(clap::Args)]
pub(crate) struct Args {
/// Python file for which to generate the AST.
#[arg(required = true)]
file: PathBuf,
/// Run in Jupyter mode i.e., allow line magics.
#[arg(long)]
jupyter: bool,
}
pub(crate) fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?;
let mode = if args.jupyter {
Mode::Ipython
} else {
Mode::Module
};
let python_ast = parse(&contents, mode, &args.file.to_string_lossy())?;
let source_type = PySourceType::from(&args.file);
let source_kind = SourceKind::from_path(&args.file, source_type)?.ok_or_else(|| {
anyhow::anyhow!(
"Could not determine source kind for file: {}",
args.file.display()
)
})?;
let python_ast = parse(
source_kind.source_code(),
source_type.as_mode(),
&args.file.to_string_lossy(),
)?;
println!("{python_ast:#?}");
Ok(())
}

View File

@@ -1,30 +1,30 @@
//! Print the token stream for a given Python file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use ruff_python_parser::{lexer, Mode};
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::PySourceType;
use ruff_python_parser::{lexer, AsMode};
#[derive(clap::Args)]
pub(crate) struct Args {
/// Python file for which to generate the AST.
#[arg(required = true)]
file: PathBuf,
/// Run in Jupyter mode i.e., allow line magics (`%`, `!`, `?`, `/`, `,`, `;`).
#[arg(long)]
jupyter: bool,
}
pub(crate) fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?;
let mode = if args.jupyter {
Mode::Ipython
} else {
Mode::Module
};
for (tok, range) in lexer::lex(&contents, mode).flatten() {
let source_type = PySourceType::from(&args.file);
let source_kind = SourceKind::from_path(&args.file, source_type)?.ok_or_else(|| {
anyhow::anyhow!(
"Could not determine source kind for file: {}",
args.file.display()
)
})?;
for (tok, range) in lexer::lex(source_kind.source_code(), source_type.as_mode()).flatten() {
println!(
"{start:#?} {tok:#?} {end:#?}",
start = range.start(),

View File

@@ -1,23 +1,9 @@
use super::{Buffer, Format, Formatter};
use crate::FormatResult;
use std::ffi::c_void;
use std::marker::PhantomData;
/// Mono-morphed type to format an object. Used by the [`crate::format`!], [`crate::format_args`!], and
/// [`crate::write`!] macros.
///
/// This struct is similar to a dynamic dispatch (using `dyn Format`) because it stores a pointer to the value.
/// However, it doesn't store the pointer to `dyn Format`'s vtable, instead it statically resolves the function
/// pointer of `Format::format` and stores it in `formatter`.
/// A convenience wrapper for representing a formattable argument.
pub struct Argument<'fmt, Context> {
/// The value to format stored as a raw pointer where `lifetime` stores the value's lifetime.
value: *const c_void,
/// Stores the lifetime of the value. To get the most out of our dear borrow checker.
lifetime: PhantomData<&'fmt ()>,
/// The function pointer to `value`'s `Format::format` method
formatter: fn(*const c_void, &mut Formatter<'_, Context>) -> FormatResult<()>,
value: &'fmt dyn Format<Context>,
}
impl<Context> Clone for Argument<'_, Context> {
@@ -28,32 +14,19 @@ impl<Context> Clone for Argument<'_, Context> {
impl<Context> Copy for Argument<'_, Context> {}
impl<'fmt, Context> Argument<'fmt, Context> {
/// Called by the [ruff_formatter::format_args] macro. Creates a mono-morphed value for formatting
/// an object.
/// Called by the [ruff_formatter::format_args] macro.
#[doc(hidden)]
#[inline]
pub fn new<F: Format<Context>>(value: &'fmt F) -> Self {
#[inline]
fn formatter<F: Format<Context>, Context>(
ptr: *const c_void,
fmt: &mut Formatter<Context>,
) -> FormatResult<()> {
// SAFETY: Safe because the 'fmt lifetime is captured by the 'lifetime' field.
#[allow(unsafe_code)]
F::fmt(unsafe { &*ptr.cast::<F>() }, fmt)
}
Self {
value: (value as *const F).cast::<std::ffi::c_void>(),
lifetime: PhantomData,
formatter: formatter::<F, Context>,
}
Self { value }
}
/// Formats the value stored by this argument using the given formatter.
#[inline]
// Seems to only be triggered on wasm32 and looks like a false positive?
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(super) fn format(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
(self.formatter)(self.value, f)
self.value.fmt(f)
}
}

View File

@@ -2555,17 +2555,17 @@ pub struct BestFitting<'a, Context> {
}
impl<'a, Context> BestFitting<'a, Context> {
/// Creates a new best fitting IR with the given variants. The method itself isn't unsafe
/// but it is to discourage people from using it because the printer will panic if
/// the slice doesn't contain at least the least and most expanded variants.
/// Creates a new best fitting IR with the given variants.
///
/// Callers are required to ensure that the number of variants given
/// is at least 2.
///
/// You're looking for a way to create a `BestFitting` object, use the `best_fitting![least_expanded, most_expanded]` macro.
///
/// ## Safety
/// The slice must contain at least two variants.
#[allow(unsafe_code)]
pub unsafe fn from_arguments_unchecked(variants: Arguments<'a, Context>) -> Self {
/// # Panics
///
/// When the slice contains less than two variants.
pub fn from_arguments_unchecked(variants: Arguments<'a, Context>) -> Self {
assert!(
variants.0.len() >= 2,
"Requires at least the least expanded and most expanded variants"
@@ -2696,14 +2696,12 @@ impl<Context> Format<Context> for BestFitting<'_, Context> {
buffer.write_element(FormatElement::Tag(EndBestFittingEntry));
}
// SAFETY: The constructor guarantees that there are always at least two variants. It's, therefore,
// safe to call into the unsafe `from_vec_unchecked` function
#[allow(unsafe_code)]
let element = unsafe {
FormatElement::BestFitting {
variants: BestFittingVariants::from_vec_unchecked(buffer.into_vec()),
mode: self.mode,
}
// OK because the constructor guarantees that there are always at
// least two variants.
let variants = BestFittingVariants::from_vec_unchecked(buffer.into_vec());
let element = FormatElement::BestFitting {
variants,
mode: self.mode,
};
f.write_element(element);

View File

@@ -332,17 +332,14 @@ pub enum BestFittingMode {
pub struct BestFittingVariants(Box<[FormatElement]>);
impl BestFittingVariants {
/// Creates a new best fitting IR with the given variants. The method itself isn't unsafe
/// but it is to discourage people from using it because the printer will panic if
/// the slice doesn't contain at least the least and most expanded variants.
/// Creates a new best fitting IR with the given variants.
///
/// Callers are required to ensure that the number of variants given
/// is at least 2 when using `most_expanded` or `most_flag`.
///
/// You're looking for a way to create a `BestFitting` object, use the `best_fitting![least_expanded, most_expanded]` macro.
///
/// ## Safety
/// The slice must contain at least two variants.
#[doc(hidden)]
#[allow(unsafe_code)]
pub unsafe fn from_vec_unchecked(variants: Vec<FormatElement>) -> Self {
pub fn from_vec_unchecked(variants: Vec<FormatElement>) -> Self {
debug_assert!(
variants
.iter()
@@ -351,12 +348,23 @@ impl BestFittingVariants {
>= 2,
"Requires at least the least expanded and most expanded variants"
);
Self(variants.into_boxed_slice())
}
/// Returns the most expanded variant
///
/// # Panics
///
/// When the number of variants is less than two.
pub fn most_expanded(&self) -> &[FormatElement] {
assert!(
self.as_slice()
.iter()
.filter(|element| matches!(element, FormatElement::Tag(Tag::StartBestFittingEntry)))
.count()
>= 2,
"Requires at least the least expanded and most expanded variants"
);
self.into_iter().last().unwrap()
}
@@ -365,7 +373,19 @@ impl BestFittingVariants {
}
/// Returns the least expanded variant
///
/// # Panics
///
/// When the number of variants is less than two.
pub fn most_flat(&self) -> &[FormatElement] {
assert!(
self.as_slice()
.iter()
.filter(|element| matches!(element, FormatElement::Tag(Tag::StartBestFittingEntry)))
.count()
>= 2,
"Requires at least the least expanded and most expanded variants"
);
self.into_iter().next().unwrap()
}
}

View File

@@ -329,10 +329,8 @@ macro_rules! format {
#[macro_export]
macro_rules! best_fitting {
($least_expanded:expr, $($tail:expr),+ $(,)?) => {{
#[allow(unsafe_code)]
unsafe {
$crate::BestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+))
}
// OK because the macro syntax requires at least two variants.
$crate::BestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+))
}}
}

View File

@@ -30,3 +30,36 @@ def func(x: int):
def func(x: int):
return 1 + 2.5 if x > 0 else 1.5 or "str"
def func(x: int):
if not x:
return None
return {"foo": 1}
def func(x: int):
return {"foo": 1}
def func(x: int):
if not x:
return 1
else:
return True
def func(x: int):
if not x:
return 1
else:
return None
def func(x: int):
if not x:
return 1
elif x > 5:
return "str"
else:
return None

View File

@@ -106,3 +106,11 @@ def func(x: bool | str):
def func(x: int | str):
pass
from typing import override
@override
def func(x: bool):
pass

View File

@@ -36,3 +36,54 @@ field10: (Literal[1] | str) | Literal[2] # Error
# Should emit for union in generic parent type.
field11: dict[Literal[1] | Literal[2], str] # Error
# Should emit for unions with more than two cases
field12: Literal[1] | Literal[2] | Literal[3] # Error
field13: Literal[1] | Literal[2] | Literal[3] | Literal[4] # Error
# Should emit for unions with more than two cases, even if not directly adjacent
field14: Literal[1] | Literal[2] | str | Literal[3] # Error
# Should emit for unions with mixed literal internal types
field15: Literal[1] | Literal["foo"] | Literal[True] # Error
# Shouldn't emit for duplicate field types with same value; covered by Y016
field16: Literal[1] | Literal[1] # OK
# Shouldn't emit if in new parent type
field17: Literal[1] | dict[Literal[2], str] # OK
# Shouldn't emit if not in a union parent
field18: dict[Literal[1], Literal[2]] # OK
# Should respect name of literal type used
field19: typing.Literal[1] | typing.Literal[2] # Error
# Should emit in cases with newlines
field20: typing.Union[
Literal[
1 # test
],
Literal[2],
] # Error, newline and comment will not be emitted in message
# Should handle multiple unions with multiple members
field21: Literal[1, 2] | Literal[3, 4] # Error
# Should emit in cases with `typing.Union` instead of `|`
field22: typing.Union[Literal[1], Literal[2]] # Error
# Should emit in cases with `typing_extensions.Literal`
field23: typing_extensions.Literal[1] | typing_extensions.Literal[2] # Error
# Should emit in cases with nested `typing.Union`
field24: typing.Union[Literal[1], typing.Union[Literal[2], str]] # Error
# Should emit in cases with mixed `typing.Union` and `|`
field25: typing.Union[Literal[1], Literal[2] | str] # Error
# Should emit only once in cases with multiple nested `typing.Union`
field24: typing.Union[Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]]] # Error
# Should use the first literal subscript attribute when fixing
field25: typing.Union[typing_extensions.Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]], str] # Error

View File

@@ -84,3 +84,6 @@ field25: typing.Union[Literal[1], Literal[2] | str] # Error
# Should emit only once in cases with multiple nested `typing.Union`
field24: typing.Union[Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]]] # Error
# Should use the first literal subscript attribute when fixing
field25: typing.Union[typing_extensions.Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]], str] # Error

View File

@@ -26,5 +26,10 @@ def func():
from trio import Event, sleep
def func():
sleep(0) # TRIO115
async def func():
await sleep(seconds=0) # TRIO115

View File

@@ -0,0 +1,34 @@
"""Test module."""
from __future__ import annotations
from functools import singledispatch
from typing import TYPE_CHECKING
from numpy import asarray
from numpy.typing import ArrayLike
from scipy.sparse import spmatrix
from pandas import DataFrame
if TYPE_CHECKING:
from numpy import ndarray
@singledispatch
def to_array_or_mat(a: ArrayLike | spmatrix) -> ndarray | spmatrix:
"""Convert arg to array or leaves it as sparse matrix."""
msg = f"Unhandled type {type(a)}"
raise NotImplementedError(msg)
@to_array_or_mat.register
def _(a: ArrayLike) -> ndarray:
return asarray(a)
@to_array_or_mat.register
def _(a: spmatrix) -> spmatrix:
return a
def _(a: DataFrame) -> DataFrame:
return a

View File

@@ -0,0 +1,3 @@
from mediuuuuuuuuuuum import a
from short import b
from loooooooooooooooooooooog import c

View File

@@ -0,0 +1,11 @@
from module1 import (
loooooooooooooong,
σηορτ,
mediuuuuum,
shoort,
looooooooooooooong,
μεδιυυυυυμ,
short,
mediuuuuuum,
λοοοοοοοοοοοοοονγ,
)

View File

@@ -0,0 +1,9 @@
import loooooooooooooong
import mediuuuuuum
import short
import σηορτ
import shoort
import mediuuuuum
import λοοοοοοοοοοοοοονγ
import μεδιυυυυυμ
import looooooooooooooong

View File

@@ -0,0 +1,6 @@
import mediuuuuuum
import short
import looooooooooooooooong
from looooooooooooooong import a
from mediuuuum import c
from short import b

View File

@@ -0,0 +1,4 @@
import mediuuuuuumb
import short
import looooooooooooooooong
import mediuuuuuuma

View File

@@ -0,0 +1,7 @@
from ..looooooooooooooong import a
from ...mediuuuum import b
from .short import c
from ....short import c
from . import d
from .mediuuuum import a
from ......short import b

View File

@@ -0,0 +1,3 @@
from looooooooooooooong import a
from mediuuuum import *
from short import *

View File

@@ -1,6 +1,6 @@
import collections
from collections import namedtuple
from typing import TypeAlias, TypeVar, NewType, NamedTuple, TypedDict
from typing import Type, TypeAlias, TypeVar, NewType, NamedTuple, TypedDict
GLOBAL: str = "foo"
@@ -40,3 +40,15 @@ def loop_assign():
global CURRENT_PORT
for CURRENT_PORT in range(5):
pass
def model_assign() -> None:
Bad = apps.get_model("zerver", "Stream") # N806
Attachment = apps.get_model("zerver", "Attachment") # OK
Recipient = apps.get_model("zerver", model_name="Recipient") # OK
Address: Type = apps.get_model("zerver", "Address") # OK
from django.utils.module_loading import import_string
Bad = import_string("django.core.exceptions.ValidationError") # N806
ValidationError = import_string("django.core.exceptions.ValidationError") # OK

View File

@@ -0,0 +1,113 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "33faf7ad-a3fd-4ac4-a0c3-52e507ed49df",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"\n",
"sys.path"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1331140f-2741-4661-9086-0764368710c9",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "a4113383-725d-4f04-80b8-a3080b2b8c4b",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.path\n",
"\n",
"import pathlib"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a5d2ef63-ae60-4311-bae3-42e845afba3f",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "79599475-a5ee-4f60-80d1-6efa77693da0",
"metadata": {},
"outputs": [],
"source": [
"import a\n",
"\n",
"try:\n",
" import b\n",
"except ImportError:\n",
" pass\n",
"else:\n",
" pass\n",
"\n",
"__some__magic = 1\n",
"\n",
"import c"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "863dcc35-5c8d-4d05-8b4a-91059e944112",
"metadata": {},
"outputs": [],
"source": [
"import ok\n",
"\n",
"\n",
"def foo() -> None:\n",
" import e\n",
"\n",
"\n",
"import no_ok"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6b2377d0-b814-4057-83ec-d443d8e19401",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff-playground)",
"language": "python",
"name": "ruff-playground"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -0,0 +1,5 @@
class Platform:
""" Remove sampler
Args:
    Returns:
"""

View File

@@ -91,3 +91,12 @@ def f(rounds: list[int], number: int) -> bool:
bool: was the round played?
"""
return number in rounds
def f():
"""
My example
==========
My example explanation
"""

View File

@@ -0,0 +1,19 @@
from random import choice
class Fruit:
COLORS = []
def __init__(self, color):
self.color = color
def pick_colors(cls, *args): # [no-classmethod-decorator]
"""classmethod to pick fruit colors"""
cls.COLORS = args
pick_colors = classmethod(pick_colors)
def pick_one_color(): # [no-staticmethod-decorator]
"""staticmethod to pick one fruit color"""
return choice(Fruit.COLORS)
pick_one_color = staticmethod(pick_one_color)

View File

@@ -0,0 +1,59 @@
import builtins
letters = ["a", "b", "c"]
def fix_these():
[letters[index] for index, letter in enumerate(letters)] # PLR1736
{letters[index] for index, letter in enumerate(letters)} # PLR1736
{letter: letters[index] for index, letter in enumerate(letters)} # PLR1736
for index, letter in enumerate(letters):
print(letters[index]) # PLR1736
blah = letters[index] # PLR1736
assert letters[index] == "d" # PLR1736
for index, letter in builtins.enumerate(letters):
print(letters[index]) # PLR1736
blah = letters[index] # PLR1736
assert letters[index] == "d" # PLR1736
def dont_fix_these():
# once there is an assignment to the sequence[index], we stop emitting diagnostics
for index, letter in enumerate(letters):
letters[index] = "d" # Ok
letters[index] += "e" # Ok
assert letters[index] == "de" # Ok
# once there is an assignment to the index, we stop emitting diagnostics
for index, letter in enumerate(letters):
index += 1 # Ok
print(letters[index]) # Ok
# once there is an assignment to the sequence, we stop emitting diagnostics
for index, letter in enumerate(letters):
letters = ["d", "e", "f"] # Ok
print(letters[index]) # Ok
# once there is an deletion from or of the sequence or index, we stop emitting diagnostics
for index, letter in enumerate(letters):
del letters[index] # Ok
print(letters[index]) # Ok
for index, letter in enumerate(letters):
del letters # Ok
print(letters[index]) # Ok
for index, letter in enumerate(letters):
del index # Ok
print(letters[index]) # Ok
def value_intentionally_unused():
[letters[index] for index, _ in enumerate(letters)] # Ok
{letters[index] for index, _ in enumerate(letters)} # Ok
{index: letters[index] for index, _ in enumerate(letters)} # Ok
for index, _ in enumerate(letters):
print(letters[index]) # Ok
blah = letters[index] # Ok
letters[index] = "d" # Ok

View File

@@ -0,0 +1,47 @@
import math
from math import e as special_e
from math import log as special_log
# Errors.
math.log(1, 2)
math.log(1, 10)
math.log(1, math.e)
foo = ...
math.log(foo, 2)
math.log(foo, 10)
math.log(foo, math.e)
math.log(1, special_e)
special_log(1, 2)
special_log(1, 10)
special_log(1, math.e)
special_log(1, special_e)
# Ok.
math.log2(1)
math.log10(1)
math.log(1)
math.log(1, 3)
math.log(1, math.pi)
two = 2
math.log(1, two)
ten = 10
math.log(1, ten)
e = math.e
math.log(1, e)
math.log2(1, 10) # math.log2 takes only one argument.
math.log10(1, 2) # math.log10 takes only one argument.
math.log(1, base=2) # math.log does not accept keyword arguments.
def log(*args):
print(f"Logging: {args}")
log(1, 2)
log(1, 10)
log(1, math.e)

View File

@@ -205,19 +205,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
ExprContext::Store => {
if checker.enabled(Rule::NonLowercaseVariableInFunction) {
if checker.semantic.current_scope().kind.is_function() {
// Ignore globals.
if !checker
.semantic
.current_scope()
.get(id)
.is_some_and(|binding_id| {
checker.semantic.binding(binding_id).is_global()
})
{
pep8_naming::rules::non_lowercase_variable_in_function(
checker, expr, id,
);
}
pep8_naming::rules::non_lowercase_variable_in_function(
checker, expr, id,
);
}
}
if checker.enabled(Rule::MixedCaseVariableInClassScope) {
@@ -380,13 +370,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flynt::rules::static_join_to_fstring(
checker,
expr,
string_value.as_str(),
string_value.to_str(),
);
}
} else if attr == "format" {
// "...".format(...) call
let location = expr.range();
match pyflakes::format::FormatSummary::try_from(string_value.as_str()) {
match pyflakes::format::FormatSummary::try_from(string_value.to_str()) {
Err(e) => {
if checker.enabled(Rule::StringDotFormatInvalidFormat) {
checker.diagnostics.push(Diagnostic::new(
@@ -432,7 +422,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::BadStringFormatCharacter) {
pylint::rules::bad_string_format_character::call(
checker,
string_value.as_str(),
string_value.to_str(),
location,
);
}
@@ -928,6 +918,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::PrintEmptyString) {
refurb::rules::print_empty_string(checker, call);
}
if checker.enabled(Rule::RedundantLogBase) {
refurb::rules::redundant_log_base(checker, call);
}
if checker.enabled(Rule::QuadraticListSummation) {
ruff::rules::quadratic_list_summation(checker, call);
}
@@ -1042,7 +1035,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::PercentFormatUnsupportedFormatCharacter,
]) {
let location = expr.range();
match pyflakes::cformat::CFormatSummary::try_from(value.as_str()) {
match pyflakes::cformat::CFormatSummary::try_from(value.to_str()) {
Err(CFormatError {
typ: CFormatErrorType::UnsupportedFormatChar(c),
..
@@ -1337,6 +1330,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
},
) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_list_set_comprehension(
checker, expr, elt, generators,
@@ -1361,6 +1357,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
},
) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_list_set_comprehension(
checker, expr, elt, generators,
@@ -1384,6 +1383,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
generators,
range: _,
}) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_dict_comprehension(
checker, expr, key, value, generators,
@@ -1408,6 +1410,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
},
) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::FunctionUsesLoopVariable) {
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr));
}

View File

@@ -384,6 +384,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
range: _,
},
) => {
if checker.enabled(Rule::NoClassmethodDecorator) {
pylint::rules::no_classmethod_decorator(checker, class_def);
}
if checker.enabled(Rule::NoStaticmethodDecorator) {
pylint::rules::no_staticmethod_decorator(checker, class_def);
}
if checker.enabled(Rule::DjangoNullableModelStringField) {
flake8_django::rules::nullable_model_string_field(checker, body);
}
@@ -1271,6 +1277,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(checker, iter, body);
}
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup(checker, for_stmt);
}
if !is_async {
if checker.enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);

View File

@@ -107,6 +107,8 @@ pub(crate) struct Checker<'a> {
pub(crate) diagnostics: Vec<Diagnostic>,
/// The list of names already seen by flake8-bugbear diagnostics, to avoid duplicate violations..
pub(crate) flake8_bugbear_seen: Vec<TextRange>,
/// The end offset of the last visited statement.
last_stmt_end: TextSize,
}
impl<'a> Checker<'a> {
@@ -142,6 +144,7 @@ impl<'a> Checker<'a> {
diagnostics: Vec::default(),
flake8_bugbear_seen: Vec::default(),
cell_offsets,
last_stmt_end: TextSize::default(),
}
}
}
@@ -268,6 +271,18 @@ where
// Step 0: Pre-processing
self.semantic.push_node(stmt);
// For Jupyter Notebooks, we'll reset the `IMPORT_BOUNDARY` flag when
// we encounter a cell boundary.
if self.source_type.is_ipynb()
&& self.semantic.at_top_level()
&& self.semantic.seen_import_boundary()
&& self.cell_offsets.is_some_and(|cell_offsets| {
cell_offsets.has_cell_boundary(TextRange::new(self.last_stmt_end, stmt.start()))
})
{
self.semantic.flags -= SemanticModelFlags::IMPORT_BOUNDARY;
}
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) => {
@@ -477,6 +492,13 @@ where
// are enabled.
let runtime_annotation = !self.semantic.future_annotations();
// The first parameter may be a single dispatch.
let mut singledispatch =
flake8_type_checking::helpers::is_singledispatch_implementation(
function_def,
self.semantic(),
);
self.semantic.push_scope(ScopeKind::Type);
if let Some(type_params) = type_params {
@@ -490,7 +512,7 @@ where
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &parameter_with_default.parameter.annotation {
if runtime_annotation {
if runtime_annotation || singledispatch {
self.visit_runtime_annotation(expr);
} else {
self.visit_annotation(expr);
@@ -499,6 +521,7 @@ where
if let Some(expr) = &parameter_with_default.default {
self.visit_expr(expr);
}
singledispatch = false;
}
if let Some(arg) = &parameters.vararg {
if let Some(expr) = &arg.annotation {
@@ -655,23 +678,24 @@ where
// available at runtime.
// See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements
let runtime_annotation = if self.semantic.future_annotations() {
if self.semantic.current_scope().kind.is_class() {
let baseclasses = &self
.settings
.flake8_type_checking
.runtime_evaluated_base_classes;
let decorators = &self
.settings
.flake8_type_checking
.runtime_evaluated_decorators;
flake8_type_checking::helpers::runtime_evaluated(
baseclasses,
decorators,
&self.semantic,
)
} else {
false
}
self.semantic
.current_scope()
.kind
.as_class()
.is_some_and(|class_def| {
flake8_type_checking::helpers::runtime_evaluated_class(
class_def,
&self
.settings
.flake8_type_checking
.runtime_evaluated_base_classes,
&self
.settings
.flake8_type_checking
.runtime_evaluated_decorators,
&self.semantic,
)
})
} else {
matches!(
self.semantic.current_scope().kind,
@@ -779,6 +803,7 @@ where
self.semantic.flags = flags_snapshot;
self.semantic.pop_node();
self.last_stmt_end = stmt.end();
}
fn visit_annotation(&mut self, expr: &'b Expr) {
@@ -799,7 +824,7 @@ where
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
self.deferred.string_type_definitions.push((
expr.range(),
value.as_str(),
value.to_str(),
self.semantic.snapshot(),
));
} else {
@@ -1219,7 +1244,7 @@ where
{
self.deferred.string_type_definitions.push((
expr.range(),
value.as_str(),
value.to_str(),
self.semantic.snapshot(),
));
}

View File

@@ -245,6 +245,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "E2515") => (RuleGroup::Stable, rules::pylint::rules::InvalidCharacterZeroWidthSpace),
(Pylint, "R0124") => (RuleGroup::Stable, rules::pylint::rules::ComparisonWithItself),
(Pylint, "R0133") => (RuleGroup::Stable, rules::pylint::rules::ComparisonOfConstant),
(Pylint, "R0202") => (RuleGroup::Preview, rules::pylint::rules::NoClassmethodDecorator),
(Pylint, "R0203") => (RuleGroup::Preview, rules::pylint::rules::NoStaticmethodDecorator),
(Pylint, "R0206") => (RuleGroup::Stable, rules::pylint::rules::PropertyWithParameters),
(Pylint, "R0402") => (RuleGroup::Stable, rules::pylint::rules::ManualFromImport),
(Pylint, "R0911") => (RuleGroup::Stable, rules::pylint::rules::TooManyReturnStatements),
@@ -258,6 +260,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison),
(Pylint, "R1706") => (RuleGroup::Preview, rules::pylint::rules::AndOrTernary),
(Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias),
(Pylint, "R1736") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryListIndexLookup),
(Pylint, "R2004") => (RuleGroup::Stable, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Stable, rules::pylint::rules::CollapsibleElseIf),
(Pylint, "R6201") => (RuleGroup::Preview, rules::pylint::rules::LiteralMembership),
@@ -955,6 +958,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
(Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate),
(Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant),
(Refurb, "163") => (RuleGroup::Preview, rules::refurb::rules::RedundantLogBase),
(Refurb, "168") => (RuleGroup::Preview, rules::refurb::rules::IsinstanceTypeNone),
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),
(Refurb, "171") => (RuleGroup::Preview, rules::refurb::rules::SingleItemMembershipTest),

View File

@@ -1,12 +1,17 @@
use itertools::Itertools;
use ruff_diagnostics::Edit;
use rustc_hash::FxHashSet;
use ruff_python_ast::helpers::{pep_604_union, ReturnStatementVisitor};
use crate::importer::{ImportRequest, Importer};
use ruff_python_ast::helpers::{
pep_604_union, typing_optional, typing_union, ReturnStatementVisitor,
};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, ExprContext};
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, SemanticModel};
use ruff_text_size::TextRange;
use ruff_text_size::{TextRange, TextSize};
use crate::settings::types::PythonVersion;
@@ -38,10 +43,7 @@ pub(crate) fn is_overload_impl(
}
/// Given a function, guess its return type.
pub(crate) fn auto_return_type(
function: &ast::StmtFunctionDef,
target_version: PythonVersion,
) -> Option<Expr> {
pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPythonType> {
// Collect all the `return` statements.
let returns = {
let mut visitor = ReturnStatementVisitor::default();
@@ -68,24 +70,94 @@ pub(crate) fn auto_return_type(
}
match return_type {
ResolvedPythonType::Atom(python_type) => type_expr(python_type),
ResolvedPythonType::Union(python_types) if target_version >= PythonVersion::Py310 => {
// Aggregate all the individual types (e.g., `int`, `float`).
let names = python_types
.iter()
.sorted_unstable()
.filter_map(|python_type| type_expr(*python_type))
.collect::<Vec<_>>();
// Wrap in a bitwise union (e.g., `int | float`).
Some(pep_604_union(&names))
}
ResolvedPythonType::Union(_) => None,
ResolvedPythonType::Atom(python_type) => Some(AutoPythonType::Atom(python_type)),
ResolvedPythonType::Union(python_types) => Some(AutoPythonType::Union(python_types)),
ResolvedPythonType::Unknown => None,
ResolvedPythonType::TypeError => None,
}
}
#[derive(Debug)]
pub(crate) enum AutoPythonType {
Atom(PythonType),
Union(FxHashSet<PythonType>),
}
impl AutoPythonType {
/// Convert an [`AutoPythonType`] into an [`Expr`].
///
/// If the [`Expr`] relies on importing any external symbols, those imports will be returned as
/// additional edits.
pub(crate) fn into_expression(
self,
importer: &Importer,
at: TextSize,
semantic: &SemanticModel,
target_version: PythonVersion,
) -> Option<(Expr, Vec<Edit>)> {
match self {
AutoPythonType::Atom(python_type) => {
let expr = type_expr(python_type)?;
Some((expr, vec![]))
}
AutoPythonType::Union(python_types) => {
if target_version >= PythonVersion::Py310 {
// Aggregate all the individual types (e.g., `int`, `float`).
let names = python_types
.iter()
.sorted_unstable()
.map(|python_type| type_expr(*python_type))
.collect::<Option<Vec<_>>>()?;
// Wrap in a bitwise union (e.g., `int | float`).
let expr = pep_604_union(&names);
Some((expr, vec![]))
} else {
let python_types = python_types
.into_iter()
.sorted_unstable()
.collect::<Vec<_>>();
match python_types.as_slice() {
[python_type, PythonType::None] | [PythonType::None, python_type] => {
let element = type_expr(*python_type)?;
// Ex) `Optional[int]`
let (optional_edit, binding) = importer
.get_or_import_symbol(
&ImportRequest::import_from("typing", "Optional"),
at,
semantic,
)
.ok()?;
let expr = typing_optional(element, binding);
Some((expr, vec![optional_edit]))
}
_ => {
let elements = python_types
.into_iter()
.map(type_expr)
.collect::<Option<Vec<_>>>()?;
// Ex) `Union[int, str]`
let (union_edit, binding) = importer
.get_or_import_symbol(
&ImportRequest::import_from("typing", "Union"),
at,
semantic,
)
.ok()?;
let expr = typing_union(&elements, binding);
Some((expr, vec![union_edit]))
}
}
}
}
}
}
}
/// Given a [`PythonType`], return an [`Expr`] that resolves to that type.
fn type_expr(python_type: PythonType) -> Option<Expr> {
fn name(name: &str) -> Expr {

View File

@@ -11,6 +11,7 @@ mod tests {
use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::types::PythonVersion;
use crate::settings::LinterSettings;
use crate::test::test_path;
@@ -128,6 +129,25 @@ mod tests {
Ok(())
}
#[test]
fn auto_return_type_py38() -> Result<()> {
let diagnostics = test_path(
Path::new("flake8_annotations/auto_return_type.py"),
&LinterSettings {
target_version: PythonVersion::Py38,
..LinterSettings::for_rules(vec![
Rule::MissingReturnTypeUndocumentedPublicFunction,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
])
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn suppress_none_returning() -> Result<()> {
let diagnostics = test_path(

View File

@@ -508,7 +508,7 @@ fn check_dynamically_typed<F>(
if let Expr::StringLiteral(ast::ExprStringLiteral { range, value }) = annotation {
// Quoted annotations
if let Ok((parsed_annotation, _)) =
parse_type_annotation(value.as_str(), *range, checker.locator().contents())
parse_type_annotation(value.to_str(), *range, checker.locator().contents())
{
if type_hint_resolves_to_any(
&parsed_annotation,
@@ -725,39 +725,55 @@ pub(crate) fn definition(
) {
if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeClassMethod) {
let return_type = auto_return_type(function, checker.settings.target_version)
.map(|return_type| checker.generator().expr(&return_type));
let return_type = auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits));
let mut diagnostic = Diagnostic::new(
MissingReturnTypeClassMethod {
name: name.to_string(),
annotation: return_type.clone(),
annotation: return_type.clone().map(|(return_type, ..)| return_type),
},
function.identifier(),
);
if let Some(return_type) = return_type {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
if let Some((return_type, edits)) = return_type {
diagnostic.set_fix(Fix::unsafe_edits(
Edit::insertion(format!(" -> {return_type}"), function.parameters.end()),
edits,
));
}
diagnostics.push(diagnostic);
}
} else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeStaticMethod) {
let return_type = auto_return_type(function, checker.settings.target_version)
.map(|return_type| checker.generator().expr(&return_type));
let return_type = auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits));
let mut diagnostic = Diagnostic::new(
MissingReturnTypeStaticMethod {
name: name.to_string(),
annotation: return_type.clone(),
annotation: return_type.clone().map(|(return_type, ..)| return_type),
},
function.identifier(),
);
if let Some(return_type) = return_type {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
if let Some((return_type, edits)) = return_type {
diagnostic.set_fix(Fix::unsafe_edits(
Edit::insertion(format!(" -> {return_type}"), function.parameters.end()),
edits,
));
}
diagnostics.push(diagnostic);
}
@@ -775,7 +791,7 @@ pub(crate) fn definition(
);
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
" -> None".to_string(),
function.parameters.range().end(),
function.parameters.end(),
)));
diagnostics.push(diagnostic);
}
@@ -793,7 +809,7 @@ pub(crate) fn definition(
if let Some(return_type) = return_type {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
function.parameters.end(),
)));
}
diagnostics.push(diagnostic);
@@ -802,42 +818,70 @@ pub(crate) fn definition(
match visibility {
visibility::Visibility::Public => {
if checker.enabled(Rule::MissingReturnTypeUndocumentedPublicFunction) {
let return_type =
auto_return_type(function, checker.settings.target_version)
.map(|return_type| checker.generator().expr(&return_type));
let return_type = auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
});
let mut diagnostic = Diagnostic::new(
MissingReturnTypeUndocumentedPublicFunction {
name: name.to_string(),
annotation: return_type.clone(),
annotation: return_type
.clone()
.map(|(return_type, ..)| return_type),
},
function.identifier(),
);
if let Some(return_type) = return_type {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
if let Some((return_type, edits)) = return_type {
diagnostic.set_fix(Fix::unsafe_edits(
Edit::insertion(
format!(" -> {return_type}"),
function.parameters.end(),
),
edits,
));
}
diagnostics.push(diagnostic);
}
}
visibility::Visibility::Private => {
if checker.enabled(Rule::MissingReturnTypePrivateFunction) {
let return_type =
auto_return_type(function, checker.settings.target_version)
.map(|return_type| checker.generator().expr(&return_type));
let return_type = auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
});
let mut diagnostic = Diagnostic::new(
MissingReturnTypePrivateFunction {
name: name.to_string(),
annotation: return_type.clone(),
annotation: return_type
.clone()
.map(|(return_type, ..)| return_type),
},
function.identifier(),
);
if let Some(return_type) = return_type {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
if let Some((return_type, edits)) = return_type {
diagnostic.set_fix(Fix::unsafe_edits(
Edit::insertion(
format!(" -> {return_type}"),
function.parameters.end(),
),
edits,
));
}
diagnostics.push(diagnostic);
}

View File

@@ -123,5 +123,81 @@ auto_return_type.py:31:5: ANN201 [*] Missing return type annotation for public f
31 |-def func(x: int):
31 |+def func(x: int) -> str | float:
32 32 | return 1 + 2.5 if x > 0 else 1.5 or "str"
33 33 |
34 34 |
auto_return_type.py:35:5: ANN201 Missing return type annotation for public function `func`
|
35 | def func(x: int):
| ^^^^ ANN201
36 | if not x:
37 | return None
|
= help: Add return type annotation
auto_return_type.py:41:5: ANN201 Missing return type annotation for public function `func`
|
41 | def func(x: int):
| ^^^^ ANN201
42 | return {"foo": 1}
|
= help: Add return type annotation
auto_return_type.py:45:5: ANN201 [*] Missing return type annotation for public function `func`
|
45 | def func(x: int):
| ^^^^ ANN201
46 | if not x:
47 | return 1
|
= help: Add return type annotation: `int`
Unsafe fix
42 42 | return {"foo": 1}
43 43 |
44 44 |
45 |-def func(x: int):
45 |+def func(x: int) -> int:
46 46 | if not x:
47 47 | return 1
48 48 | else:
auto_return_type.py:52:5: ANN201 [*] Missing return type annotation for public function `func`
|
52 | def func(x: int):
| ^^^^ ANN201
53 | if not x:
54 | return 1
|
= help: Add return type annotation: `int | None`
Unsafe fix
49 49 | return True
50 50 |
51 51 |
52 |-def func(x: int):
52 |+def func(x: int) -> int | None:
53 53 | if not x:
54 54 | return 1
55 55 | else:
auto_return_type.py:59:5: ANN201 [*] Missing return type annotation for public function `func`
|
59 | def func(x: int):
| ^^^^ ANN201
60 | if not x:
61 | return 1
|
= help: Add return type annotation: `str | int | None`
Unsafe fix
56 56 | return None
57 57 |
58 58 |
59 |-def func(x: int):
59 |+def func(x: int) -> str | int | None:
60 60 | if not x:
61 61 | return 1
62 62 | elif x > 5:

View File

@@ -0,0 +1,223 @@
---
source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs
---
auto_return_type.py:1:5: ANN201 [*] Missing return type annotation for public function `func`
|
1 | def func():
| ^^^^ ANN201
2 | return 1
|
= help: Add return type annotation: `int`
Unsafe fix
1 |-def func():
1 |+def func() -> int:
2 2 | return 1
3 3 |
4 4 |
auto_return_type.py:5:5: ANN201 [*] Missing return type annotation for public function `func`
|
5 | def func():
| ^^^^ ANN201
6 | return 1.5
|
= help: Add return type annotation: `float`
Unsafe fix
2 2 | return 1
3 3 |
4 4 |
5 |-def func():
5 |+def func() -> float:
6 6 | return 1.5
7 7 |
8 8 |
auto_return_type.py:9:5: ANN201 [*] Missing return type annotation for public function `func`
|
9 | def func(x: int):
| ^^^^ ANN201
10 | if x > 0:
11 | return 1
|
= help: Add return type annotation: `float`
Unsafe fix
6 6 | return 1.5
7 7 |
8 8 |
9 |-def func(x: int):
9 |+def func(x: int) -> float:
10 10 | if x > 0:
11 11 | return 1
12 12 | else:
auto_return_type.py:16:5: ANN201 [*] Missing return type annotation for public function `func`
|
16 | def func():
| ^^^^ ANN201
17 | return True
|
= help: Add return type annotation: `bool`
Unsafe fix
13 13 | return 1.5
14 14 |
15 15 |
16 |-def func():
16 |+def func() -> bool:
17 17 | return True
18 18 |
19 19 |
auto_return_type.py:20:5: ANN201 [*] Missing return type annotation for public function `func`
|
20 | def func(x: int):
| ^^^^ ANN201
21 | if x > 0:
22 | return None
|
= help: Add return type annotation: `None`
Unsafe fix
17 17 | return True
18 18 |
19 19 |
20 |-def func(x: int):
20 |+def func(x: int) -> None:
21 21 | if x > 0:
22 22 | return None
23 23 | else:
auto_return_type.py:27:5: ANN201 [*] Missing return type annotation for public function `func`
|
27 | def func(x: int):
| ^^^^ ANN201
28 | return 1 or 2.5 if x > 0 else 1.5 or "str"
|
= help: Add return type annotation: `Union[str | float]`
Unsafe fix
1 |+from typing import Union
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
24 25 | return
25 26 |
26 27 |
27 |-def func(x: int):
28 |+def func(x: int) -> Union[str | float]:
28 29 | return 1 or 2.5 if x > 0 else 1.5 or "str"
29 30 |
30 31 |
auto_return_type.py:31:5: ANN201 [*] Missing return type annotation for public function `func`
|
31 | def func(x: int):
| ^^^^ ANN201
32 | return 1 + 2.5 if x > 0 else 1.5 or "str"
|
= help: Add return type annotation: `Union[str | float]`
Unsafe fix
1 |+from typing import Union
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
28 29 | return 1 or 2.5 if x > 0 else 1.5 or "str"
29 30 |
30 31 |
31 |-def func(x: int):
32 |+def func(x: int) -> Union[str | float]:
32 33 | return 1 + 2.5 if x > 0 else 1.5 or "str"
33 34 |
34 35 |
auto_return_type.py:35:5: ANN201 Missing return type annotation for public function `func`
|
35 | def func(x: int):
| ^^^^ ANN201
36 | if not x:
37 | return None
|
= help: Add return type annotation
auto_return_type.py:41:5: ANN201 Missing return type annotation for public function `func`
|
41 | def func(x: int):
| ^^^^ ANN201
42 | return {"foo": 1}
|
= help: Add return type annotation
auto_return_type.py:45:5: ANN201 [*] Missing return type annotation for public function `func`
|
45 | def func(x: int):
| ^^^^ ANN201
46 | if not x:
47 | return 1
|
= help: Add return type annotation: `int`
Unsafe fix
42 42 | return {"foo": 1}
43 43 |
44 44 |
45 |-def func(x: int):
45 |+def func(x: int) -> int:
46 46 | if not x:
47 47 | return 1
48 48 | else:
auto_return_type.py:52:5: ANN201 [*] Missing return type annotation for public function `func`
|
52 | def func(x: int):
| ^^^^ ANN201
53 | if not x:
54 | return 1
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
49 50 | return True
50 51 |
51 52 |
52 |-def func(x: int):
53 |+def func(x: int) -> Optional[int]:
53 54 | if not x:
54 55 | return 1
55 56 | else:
auto_return_type.py:59:5: ANN201 [*] Missing return type annotation for public function `func`
|
59 | def func(x: int):
| ^^^^ ANN201
60 | if not x:
61 | return 1
|
= help: Add return type annotation: `Union[str | int | None]`
Unsafe fix
1 |+from typing import Union
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
56 57 | return None
57 58 |
58 59 |
59 |-def func(x: int):
60 |+def func(x: int) -> Union[str | int | None]:
60 61 | if not x:
61 62 | return 1
62 63 | elif x > 5:

View File

@@ -10,7 +10,7 @@ static PASSWORD_CANDIDATE_REGEX: Lazy<Regex> = Lazy::new(|| {
pub(super) fn string_literal(expr: &Expr) -> Option<&str> {
match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => Some(value.as_str()),
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => Some(value.to_str()),
_ => None,
}
}

View File

@@ -35,7 +35,7 @@ impl Violation for HardcodedBindAllInterfaces {
/// S104
pub(crate) fn hardcoded_bind_all_interfaces(string: &ExprStringLiteral) -> Option<Diagnostic> {
if string.value.as_str() == "0.0.0.0" {
if string.value.to_str() == "0.0.0.0" {
Some(Diagnostic::new(HardcodedBindAllInterfaces, string.range))
} else {
None

View File

@@ -55,7 +55,7 @@ fn password_target(target: &Expr) -> Option<&str> {
Expr::Name(ast::ExprName { id, .. }) => id.as_str(),
// d["password"] = "s3cr3t"
Expr::Subscript(ast::ExprSubscript { slice, .. }) => match slice.as_ref() {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.as_str(),
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.to_str(),
_ => return None,
},
// obj.password = "s3cr3t"

View File

@@ -93,7 +93,7 @@ pub(crate) fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
let Some(string) = left.as_string_literal_expr() else {
return;
};
string.value.as_str().escape_default().to_string()
string.value.to_str().escape_default().to_string()
}
Expr::Call(ast::ExprCall { func, .. }) => {
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else {
@@ -106,7 +106,7 @@ pub(crate) fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
let Some(string) = value.as_string_literal_expr() else {
return;
};
string.value.as_str().escape_default().to_string()
string.value.to_str().escape_default().to_string()
}
// f"select * from table where val = {val}"
Expr::FString(f_string) => concatenated_f_string(f_string, checker.locator()),

View File

@@ -57,7 +57,7 @@ pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: &ast::ExprS
.flake8_bandit
.hardcoded_tmp_directory
.iter()
.any(|prefix| string.value.as_str().starts_with(prefix))
.any(|prefix| string.value.to_str().starts_with(prefix))
{
return;
}

View File

@@ -855,7 +855,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
// If the `url` argument is a string literal, allow `http` and `https` schemes.
if call.arguments.args.iter().all(|arg| !arg.is_starred_expr()) && call.arguments.keywords.iter().all(|keyword| keyword.arg.is_some()) {
if let Some(Expr::StringLiteral(ast::ExprStringLiteral { value, .. })) = &call.arguments.find_argument("url", 0) {
let url = value.as_str().trim_start();
let url = value.to_str().trim_start();
if url.starts_with("http://") || url.starts_with("https://") {
return None;
}

View File

@@ -60,7 +60,7 @@ pub(crate) fn tarfile_unsafe_members(checker: &mut Checker, call: &ast::ExprCall
.arguments
.find_keyword("filter")
.and_then(|keyword| keyword.value.as_string_literal_expr())
.is_some_and(|value| matches!(value.value.as_str(), "data" | "tar"))
.is_some_and(|value| matches!(value.value.to_str(), "data" | "tar"))
{
return;
}

View File

@@ -2,6 +2,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::collect_call_path;
use ruff_python_ast::{Decorator, ParameterWithDefault, Parameters};
use ruff_python_semantic::analyze::visibility;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -94,23 +95,18 @@ impl Violation for BooleanDefaultValuePositionalArgument {
}
}
/// FBT002
pub(crate) fn boolean_default_value_positional_argument(
checker: &mut Checker,
name: &str,
decorator_list: &[Decorator],
parameters: &Parameters,
) {
// Allow Boolean defaults in explicitly-allowed functions.
if is_allowed_func_def(name) {
return;
}
if decorator_list.iter().any(|decorator| {
collect_call_path(&decorator.expression)
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
}) {
return;
}
for ParameterWithDefault {
parameter,
default,
@@ -121,6 +117,20 @@ pub(crate) fn boolean_default_value_positional_argument(
.as_ref()
.is_some_and(|default| default.is_boolean_literal_expr())
{
// Allow Boolean defaults in setters.
if decorator_list.iter().any(|decorator| {
collect_call_path(&decorator.expression)
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
}) {
return;
}
// Allow Boolean defaults in `@override` methods, since they're required to adhere to
// the parent signature.
if visibility::is_override(decorator_list, checker.semantic()) {
return;
}
checker.diagnostics.push(Diagnostic::new(
BooleanDefaultValuePositionalArgument,
parameter.name.range(),

View File

@@ -4,6 +4,7 @@ use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::collect_call_path;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
@@ -109,17 +110,11 @@ pub(crate) fn boolean_type_hint_positional_argument(
decorator_list: &[Decorator],
parameters: &Parameters,
) {
// Allow Boolean type hints in explicitly-allowed functions.
if is_allowed_func_def(name) {
return;
}
if decorator_list.iter().any(|decorator| {
collect_call_path(&decorator.expression)
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
}) {
return;
}
for ParameterWithDefault {
parameter,
default: _,
@@ -138,9 +133,26 @@ pub(crate) fn boolean_type_hint_positional_argument(
continue;
}
}
// Allow Boolean type hints in setters.
if decorator_list.iter().any(|decorator| {
collect_call_path(&decorator.expression)
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
}) {
return;
}
// Allow Boolean defaults in `@override` methods, since they're required to adhere to
// the parent signature.
if visibility::is_override(decorator_list, checker.semantic()) {
return;
}
// If `bool` isn't actually a reference to the `bool` built-in, return.
if !checker.semantic().is_builtin("bool") {
return;
}
checker.diagnostics.push(Diagnostic::new(
BooleanTypeHintPositionalArgument,
parameter.name.range(),

View File

@@ -69,10 +69,10 @@ pub(crate) fn getattr_with_constant(
let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = arg else {
return;
};
if !is_identifier(value.as_str()) {
if !is_identifier(value.to_str()) {
return;
}
if is_mangled_private(value.as_str()) {
if is_mangled_private(value.to_str()) {
return;
}
if !checker.semantic().is_builtin("getattr") {

View File

@@ -83,10 +83,10 @@ pub(crate) fn setattr_with_constant(
let Expr::StringLiteral(ast::ExprStringLiteral { value: name, .. }) = name else {
return;
};
if !is_identifier(name.as_str()) {
if !is_identifier(name.to_str()) {
return;
}
if is_mangled_private(name.as_str()) {
if is_mangled_private(name.to_str()) {
return;
}
if !checker.semantic().is_builtin("setattr") {
@@ -104,7 +104,7 @@ pub(crate) fn setattr_with_constant(
if expr == child.as_ref() {
let mut diagnostic = Diagnostic::new(SetAttrWithConstant, expr.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
assignment(obj, name.as_str(), value, checker.generator()),
assignment(obj, name.to_str(), value, checker.generator()),
expr.range(),
)));
checker.diagnostics.push(diagnostic);

View File

@@ -30,6 +30,14 @@ use crate::rules::flake8_comprehensions::fixes;
/// ```python
/// sorted(iterable, reverse=True)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as `reversed` and `reverse=True` will
/// yield different results in the event of custom sort keys or equality
/// functions. Specifically, `reversed` will reverse the order of the
/// collection, while `sorted` with `reverse=True` will perform a stable
/// reverse sort, which will preserve the order of elements that compare as
/// equal.
#[violation]
pub struct UnnecessaryCallAroundSorted {
func: String,

View File

@@ -32,6 +32,10 @@ use crate::rules::flake8_comprehensions::settings::Settings;
/// []
/// ()
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryCollectionCall {
obj_type: String,

View File

@@ -29,6 +29,11 @@ use crate::rules::flake8_comprehensions::fixes;
/// list(iterable)
/// set(iterable)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the comprehension. In most cases, though, comments will be
/// preserved.
#[violation]
pub struct UnnecessaryComprehension {
obj_type: String,

View File

@@ -40,6 +40,11 @@ use crate::rules::flake8_comprehensions::fixes;
/// any(x.id for x in bar)
/// all(x.id for x in bar)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the comprehension. In most cases, though, comments will be
/// preserved.
#[violation]
pub struct UnnecessaryComprehensionAnyAll;

View File

@@ -43,6 +43,10 @@ use crate::rules::flake8_comprehensions::fixes;
/// - Instead of `sorted(tuple(iterable))`, use `sorted(iterable)`.
/// - Instead of `sorted(sorted(iterable))`, use `sorted(iterable)`.
/// - Instead of `sorted(reversed(iterable))`, use `sorted(iterable)`.
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryDoubleCastOrProcess {
inner: String,

View File

@@ -27,6 +27,10 @@ use super::helpers;
/// ```python
/// {x: f(x) for x in foo}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryGeneratorDict;

View File

@@ -28,6 +28,10 @@ use super::helpers;
/// ```python
/// [f(x) for x in foo]
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryGeneratorList;

View File

@@ -28,6 +28,10 @@ use super::helpers;
/// ```python
/// {f(x) for x in foo}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryGeneratorSet;

View File

@@ -25,6 +25,10 @@ use super::helpers;
/// ```python
/// [f(x) for x in foo]
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryListCall;

View File

@@ -25,6 +25,10 @@ use super::helpers;
/// ```python
/// {x: f(x) for x in foo}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryListComprehensionDict;

View File

@@ -26,6 +26,10 @@ use super::helpers;
/// ```python
/// {f(x) for x in foo}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryListComprehensionSet;

View File

@@ -29,6 +29,10 @@ use super::helpers;
/// {1: 2, 3: 4}
/// {}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryLiteralDict {
obj_type: String,

View File

@@ -31,6 +31,10 @@ use super::helpers;
/// {1, 2}
/// set()
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryLiteralSet {
obj_type: String,

View File

@@ -46,6 +46,10 @@ impl fmt::Display for DictKind {
/// {}
/// {"a": 1}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryLiteralWithinDictCall {
kind: DictKind,

View File

@@ -32,6 +32,10 @@ use super::helpers;
/// [1, 2]
/// [1, 2]
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryLiteralWithinListCall {
literal: String,

View File

@@ -33,6 +33,10 @@ use super::helpers;
/// (1, 2)
/// (1, 2)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryLiteralWithinTupleCall {
literal: String,

View File

@@ -22,6 +22,16 @@ use super::helpers;
/// using a generator expression or a comprehension, as the latter approach
/// avoids the function call overhead, in addition to being more readable.
///
/// This rule also applies to `map` calls within `list`, `set`, and `dict`
/// calls. For example:
///
/// - Instead of `list(map(lambda num: num * 2, nums))`, use
/// `[num * 2 for num in nums]`.
/// - Instead of `set(map(lambda num: num % 2 == 0, nums))`, use
/// `{num % 2 == 0 for num in nums}`.
/// - Instead of `dict(map(lambda v: (v, v ** 2), values))`, use
/// `{v: v ** 2 for v in values}`.
///
/// ## Examples
/// ```python
/// map(lambda x: x + 1, iterable)
@@ -32,15 +42,9 @@ use super::helpers;
/// (x + 1 for x in iterable)
/// ```
///
/// This rule also applies to `map` calls within `list`, `set`, and `dict`
/// calls. For example:
///
/// - Instead of `list(map(lambda num: num * 2, nums))`, use
/// `[num * 2 for num in nums]`.
/// - Instead of `set(map(lambda num: num % 2 == 0, nums))`, use
/// `{num % 2 == 0 for num in nums}`.
/// - Instead of `dict(map(lambda v: (v, v ** 2), values))`, use
/// `{v: v ** 2 for v in values}`.
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
/// when rewriting the call. In most cases, though, comments will be preserved.
#[violation]
pub struct UnnecessaryMap {
object_type: ObjectType,

View File

@@ -78,7 +78,7 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &mut Checker, call: &
if let Some(Expr::StringLiteral(ast::ExprStringLiteral { value: format, .. })) =
call.arguments.args.get(1).as_ref()
{
if format.as_str().contains("%z") {
if format.to_str().contains("%z") {
return;
}
};

View File

@@ -93,7 +93,7 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) {
for key in keys {
if let Some(key) = &key {
if let Expr::StringLiteral(ast::ExprStringLiteral { value: attr, .. }) = key {
if is_reserved_attr(attr.as_str()) {
if is_reserved_attr(attr.to_str()) {
checker.diagnostics.push(Diagnostic::new(
LoggingExtraAttrClash(attr.to_string()),
key.range(),

View File

@@ -110,8 +110,8 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs
/// Return `Some` if a key is a valid keyword argument name, or `None` otherwise.
fn as_kwarg(key: &Expr) -> Option<&str> {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = key {
if is_identifier(value.as_str()) {
return Some(value.as_str());
if is_identifier(value.to_str()) {
return Some(value.to_str());
}
}
None

View File

@@ -1,9 +1,11 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ast::{ExprSubscript, Operator};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::rules::flake8_pyi::helpers::traverse_union;
/// ## What it does
@@ -32,6 +34,8 @@ pub struct UnnecessaryLiteralUnion {
}
impl Violation for UnnecessaryLiteralUnion {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!(
@@ -39,36 +43,153 @@ impl Violation for UnnecessaryLiteralUnion {
self.members.join(", ")
)
}
fn fix_title(&self) -> Option<String> {
Some(format!("Replace with a single `Literal`",))
}
}
fn concatenate_bin_ors(exprs: Vec<&Expr>) -> Expr {
let mut exprs = exprs.into_iter();
let first = exprs.next().unwrap();
exprs.fold((*first).clone(), |acc, expr| {
Expr::BinOp(ast::ExprBinOp {
left: Box::new(acc),
op: Operator::BitOr,
right: Box::new((*expr).clone()),
range: TextRange::default(),
})
})
}
fn make_union(subscript: &ExprSubscript, exprs: Vec<&Expr>) -> Expr {
Expr::Subscript(ast::ExprSubscript {
value: subscript.value.clone(),
slice: Box::new(Expr::Tuple(ast::ExprTuple {
elts: exprs.into_iter().map(|expr| (*expr).clone()).collect(),
range: TextRange::default(),
ctx: ast::ExprContext::Load,
})),
range: TextRange::default(),
ctx: ast::ExprContext::Load,
})
}
fn make_literal_expr(subscript: Option<Expr>, exprs: Vec<&Expr>) -> Expr {
let use_subscript = if let subscript @ Some(_) = subscript {
subscript.unwrap().clone()
} else {
Expr::Name(ast::ExprName {
id: "Literal".to_string(),
range: TextRange::default(),
ctx: ast::ExprContext::Load,
})
};
Expr::Subscript(ast::ExprSubscript {
value: Box::new(use_subscript),
slice: Box::new(Expr::Tuple(ast::ExprTuple {
elts: exprs.into_iter().map(|expr| (*expr).clone()).collect(),
range: TextRange::default(),
ctx: ast::ExprContext::Load,
})),
range: TextRange::default(),
ctx: ast::ExprContext::Load,
})
}
/// PYI030
pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Expr) {
let mut literal_exprs = Vec::new();
let mut other_exprs = Vec::new();
// Adds a member to `literal_exprs` if it is a `Literal` annotation
// for the sake of consistency and correctness, we'll use the first `Literal` subscript attribute
// to construct the fix
let mut literal_subscript = None;
let mut total_literals = 0;
// Split members into `literal_exprs` if they are a `Literal` annotation and `other_exprs` otherwise
let mut collect_literal_expr = |expr: &'a Expr, _| {
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
if checker.semantic().match_typing_expr(value, "Literal") {
literal_exprs.push(slice);
total_literals += 1;
if literal_subscript.is_none() {
literal_subscript = Some(*value.clone());
}
// flatten already-unioned literals to later union again
if let Expr::Tuple(ast::ExprTuple {
elts,
range: _,
ctx: _,
}) = slice.as_ref()
{
for expr in elts {
literal_exprs.push(expr);
}
} else {
literal_exprs.push(slice.as_ref());
}
}
} else {
other_exprs.push(expr);
}
};
// Traverse the union, collect all literal members
// Traverse the union, collect all members, split out the literals from the rest.
traverse_union(&mut collect_literal_expr, checker.semantic(), expr, None);
// Raise a violation if more than one
if literal_exprs.len() > 1 {
let diagnostic = Diagnostic::new(
let union_subscript = expr.as_subscript_expr();
if union_subscript.is_some_and(|subscript| {
!checker
.semantic()
.match_typing_expr(&subscript.value, "Union")
}) {
return;
}
// Raise a violation if more than one.
if total_literals > 1 {
let literal_members: Vec<String> = literal_exprs
.clone()
.into_iter()
.map(|expr| checker.locator().slice(expr).to_string())
.collect();
let mut diagnostic = Diagnostic::new(
UnnecessaryLiteralUnion {
members: literal_exprs
.into_iter()
.map(|expr| checker.locator().slice(expr.as_ref()).to_string())
.collect(),
members: literal_members.clone(),
},
expr.range(),
);
if checker.settings.preview.is_enabled() {
let literals =
make_literal_expr(literal_subscript, literal_exprs.into_iter().collect());
if other_exprs.is_empty() {
// if the union is only literals, we just replace the whole thing with a single literal
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
checker.generator().expr(&literals),
expr.range(),
)));
} else {
let mut expr_vec: Vec<&Expr> = other_exprs.clone().into_iter().collect();
expr_vec.insert(0, &literals);
let content = if let Some(subscript) = union_subscript {
checker.generator().expr(&make_union(subscript, expr_vec))
} else {
checker.generator().expr(&concatenate_bin_ors(expr_vec))
};
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
content,
expr.range(),
)));
}
}
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -127,7 +127,7 @@ pub(crate) fn unrecognized_platform(checker: &mut Checker, test: &Expr) {
// Other values are possible but we don't need them right now.
// This protects against typos.
if checker.enabled(Rule::UnrecognizedPlatformName) {
if !matches!(value.as_str(), "linux" | "win32" | "cygwin" | "darwin") {
if !matches!(value.to_str(), "linux" | "win32" | "cygwin" | "darwin") {
checker.diagnostics.push(Diagnostic::new(
UnrecognizedPlatformName {
platform: value.to_string(),

View File

@@ -9,6 +9,7 @@ PYI030.py:9:9: PYI030 Multiple literal members in a union. Use a single literal,
10 |
11 | # Should emit for union types in arguments.
|
= help: Replace with a single `Literal`
PYI030.py:12:17: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -17,6 +18,7 @@ PYI030.py:12:17: PYI030 Multiple literal members in a union. Use a single litera
| ^^^^^^^^^^^^^^^^^^^^^^^ PYI030
13 | print(arg1)
|
= help: Replace with a single `Literal`
PYI030.py:17:16: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -25,6 +27,7 @@ PYI030.py:17:16: PYI030 Multiple literal members in a union. Use a single litera
| ^^^^^^^^^^^^^^^^^^^^^^^ PYI030
18 | return "my Literal[1]ing"
|
= help: Replace with a single `Literal`
PYI030.py:22:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -34,6 +37,7 @@ PYI030.py:22:9: PYI030 Multiple literal members in a union. Use a single literal
23 | field4: str | Literal[1] | Literal[2] # Error
24 | field5: Literal[1] | str | Literal[2] # Error
|
= help: Replace with a single `Literal`
PYI030.py:23:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -44,6 +48,7 @@ PYI030.py:23:9: PYI030 Multiple literal members in a union. Use a single literal
24 | field5: Literal[1] | str | Literal[2] # Error
25 | field6: Literal[1] | bool | Literal[2] | str # Error
|
= help: Replace with a single `Literal`
PYI030.py:24:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -53,6 +58,7 @@ PYI030.py:24:9: PYI030 Multiple literal members in a union. Use a single literal
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
25 | field6: Literal[1] | bool | Literal[2] | str # Error
|
= help: Replace with a single `Literal`
PYI030.py:25:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -63,6 +69,7 @@ PYI030.py:25:9: PYI030 Multiple literal members in a union. Use a single literal
26 |
27 | # Should emit for non-type unions.
|
= help: Replace with a single `Literal`
PYI030.py:28:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -72,6 +79,7 @@ PYI030.py:28:10: PYI030 Multiple literal members in a union. Use a single litera
29 |
30 | # Should emit for parenthesized unions.
|
= help: Replace with a single `Literal`
PYI030.py:31:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -81,6 +89,7 @@ PYI030.py:31:9: PYI030 Multiple literal members in a union. Use a single literal
32 |
33 | # Should handle user parentheses when fixing.
|
= help: Replace with a single `Literal`
PYI030.py:34:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -89,6 +98,7 @@ PYI030.py:34:9: PYI030 Multiple literal members in a union. Use a single literal
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
35 | field10: (Literal[1] | str) | Literal[2] # Error
|
= help: Replace with a single `Literal`
PYI030.py:35:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -99,12 +109,160 @@ PYI030.py:35:10: PYI030 Multiple literal members in a union. Use a single litera
36 |
37 | # Should emit for union in generic parent type.
|
= help: Replace with a single `Literal`
PYI030.py:38:15: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
37 | # Should emit for union in generic parent type.
38 | field11: dict[Literal[1] | Literal[2], str] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^ PYI030
39 |
40 | # Should emit for unions with more than two cases
|
= help: Replace with a single `Literal`
PYI030.py:41:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3]`
|
40 | # Should emit for unions with more than two cases
41 | field12: Literal[1] | Literal[2] | Literal[3] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
42 | field13: Literal[1] | Literal[2] | Literal[3] | Literal[4] # Error
|
= help: Replace with a single `Literal`
PYI030.py:42:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]`
|
40 | # Should emit for unions with more than two cases
41 | field12: Literal[1] | Literal[2] | Literal[3] # Error
42 | field13: Literal[1] | Literal[2] | Literal[3] | Literal[4] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
43 |
44 | # Should emit for unions with more than two cases, even if not directly adjacent
|
= help: Replace with a single `Literal`
PYI030.py:45:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3]`
|
44 | # Should emit for unions with more than two cases, even if not directly adjacent
45 | field14: Literal[1] | Literal[2] | str | Literal[3] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
46 |
47 | # Should emit for unions with mixed literal internal types
|
= help: Replace with a single `Literal`
PYI030.py:48:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, "foo", True]`
|
47 | # Should emit for unions with mixed literal internal types
48 | field15: Literal[1] | Literal["foo"] | Literal[True] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
49 |
50 | # Shouldn't emit for duplicate field types with same value; covered by Y016
|
= help: Replace with a single `Literal`
PYI030.py:51:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 1]`
|
50 | # Shouldn't emit for duplicate field types with same value; covered by Y016
51 | field16: Literal[1] | Literal[1] # OK
| ^^^^^^^^^^^^^^^^^^^^^^^ PYI030
52 |
53 | # Shouldn't emit if in new parent type
|
= help: Replace with a single `Literal`
PYI030.py:60:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
59 | # Should respect name of literal type used
60 | field19: typing.Literal[1] | typing.Literal[2] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
61 |
62 | # Should emit in cases with newlines
|
= help: Replace with a single `Literal`
PYI030.py:63:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
62 | # Should emit in cases with newlines
63 | field20: typing.Union[
| __________^
64 | | Literal[
65 | | 1 # test
66 | | ],
67 | | Literal[2],
68 | | ] # Error, newline and comment will not be emitted in message
| |_^ PYI030
69 |
70 | # Should handle multiple unions with multiple members
|
= help: Replace with a single `Literal`
PYI030.py:71:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]`
|
70 | # Should handle multiple unions with multiple members
71 | field21: Literal[1, 2] | Literal[3, 4] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
72 |
73 | # Should emit in cases with `typing.Union` instead of `|`
|
= help: Replace with a single `Literal`
PYI030.py:74:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
73 | # Should emit in cases with `typing.Union` instead of `|`
74 | field22: typing.Union[Literal[1], Literal[2]] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
75 |
76 | # Should emit in cases with `typing_extensions.Literal`
|
= help: Replace with a single `Literal`
PYI030.py:77:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
76 | # Should emit in cases with `typing_extensions.Literal`
77 | field23: typing_extensions.Literal[1] | typing_extensions.Literal[2] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
78 |
79 | # Should emit in cases with nested `typing.Union`
|
= help: Replace with a single `Literal`
PYI030.py:80:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
79 | # Should emit in cases with nested `typing.Union`
80 | field24: typing.Union[Literal[1], typing.Union[Literal[2], str]] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
81 |
82 | # Should emit in cases with mixed `typing.Union` and `|`
|
= help: Replace with a single `Literal`
PYI030.py:83:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
82 | # Should emit in cases with mixed `typing.Union` and `|`
83 | field25: typing.Union[Literal[1], Literal[2] | str] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
84 |
85 | # Should emit only once in cases with multiple nested `typing.Union`
|
= help: Replace with a single `Literal`
PYI030.py:86:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]`
|
85 | # Should emit only once in cases with multiple nested `typing.Union`
86 | field24: typing.Union[Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]]] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
87 |
88 | # Should use the first literal subscript attribute when fixing
|
= help: Replace with a single `Literal`
PYI030.py:89:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]`
|
88 | # Should use the first literal subscript attribute when fixing
89 | field25: typing.Union[typing_extensions.Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]], str] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
|
= help: Replace with a single `Literal`

View File

@@ -9,6 +9,7 @@ PYI030.pyi:9:9: PYI030 Multiple literal members in a union. Use a single literal
10 |
11 | # Should emit for union types in arguments.
|
= help: Replace with a single `Literal`
PYI030.pyi:12:17: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -17,6 +18,7 @@ PYI030.pyi:12:17: PYI030 Multiple literal members in a union. Use a single liter
| ^^^^^^^^^^^^^^^^^^^^^^^ PYI030
13 | print(arg1)
|
= help: Replace with a single `Literal`
PYI030.pyi:17:16: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -25,6 +27,7 @@ PYI030.pyi:17:16: PYI030 Multiple literal members in a union. Use a single liter
| ^^^^^^^^^^^^^^^^^^^^^^^ PYI030
18 | return "my Literal[1]ing"
|
= help: Replace with a single `Literal`
PYI030.pyi:22:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -34,6 +37,7 @@ PYI030.pyi:22:9: PYI030 Multiple literal members in a union. Use a single litera
23 | field4: str | Literal[1] | Literal[2] # Error
24 | field5: Literal[1] | str | Literal[2] # Error
|
= help: Replace with a single `Literal`
PYI030.pyi:23:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -44,6 +48,7 @@ PYI030.pyi:23:9: PYI030 Multiple literal members in a union. Use a single litera
24 | field5: Literal[1] | str | Literal[2] # Error
25 | field6: Literal[1] | bool | Literal[2] | str # Error
|
= help: Replace with a single `Literal`
PYI030.pyi:24:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -53,6 +58,7 @@ PYI030.pyi:24:9: PYI030 Multiple literal members in a union. Use a single litera
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
25 | field6: Literal[1] | bool | Literal[2] | str # Error
|
= help: Replace with a single `Literal`
PYI030.pyi:25:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -63,6 +69,7 @@ PYI030.pyi:25:9: PYI030 Multiple literal members in a union. Use a single litera
26 |
27 | # Should emit for non-type unions.
|
= help: Replace with a single `Literal`
PYI030.pyi:28:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -72,6 +79,7 @@ PYI030.pyi:28:10: PYI030 Multiple literal members in a union. Use a single liter
29 |
30 | # Should emit for parenthesized unions.
|
= help: Replace with a single `Literal`
PYI030.pyi:31:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -81,6 +89,7 @@ PYI030.pyi:31:9: PYI030 Multiple literal members in a union. Use a single litera
32 |
33 | # Should handle user parentheses when fixing.
|
= help: Replace with a single `Literal`
PYI030.pyi:34:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -89,6 +98,7 @@ PYI030.pyi:34:9: PYI030 Multiple literal members in a union. Use a single litera
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
35 | field10: (Literal[1] | str) | Literal[2] # Error
|
= help: Replace with a single `Literal`
PYI030.pyi:35:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -99,6 +109,7 @@ PYI030.pyi:35:10: PYI030 Multiple literal members in a union. Use a single liter
36 |
37 | # Should emit for union in generic parent type.
|
= help: Replace with a single `Literal`
PYI030.pyi:38:15: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -108,6 +119,7 @@ PYI030.pyi:38:15: PYI030 Multiple literal members in a union. Use a single liter
39 |
40 | # Should emit for unions with more than two cases
|
= help: Replace with a single `Literal`
PYI030.pyi:41:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3]`
|
@@ -116,6 +128,7 @@ PYI030.pyi:41:10: PYI030 Multiple literal members in a union. Use a single liter
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
42 | field13: Literal[1] | Literal[2] | Literal[3] | Literal[4] # Error
|
= help: Replace with a single `Literal`
PYI030.pyi:42:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]`
|
@@ -126,6 +139,7 @@ PYI030.pyi:42:10: PYI030 Multiple literal members in a union. Use a single liter
43 |
44 | # Should emit for unions with more than two cases, even if not directly adjacent
|
= help: Replace with a single `Literal`
PYI030.pyi:45:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3]`
|
@@ -135,6 +149,7 @@ PYI030.pyi:45:10: PYI030 Multiple literal members in a union. Use a single liter
46 |
47 | # Should emit for unions with mixed literal internal types
|
= help: Replace with a single `Literal`
PYI030.pyi:48:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, "foo", True]`
|
@@ -144,6 +159,7 @@ PYI030.pyi:48:10: PYI030 Multiple literal members in a union. Use a single liter
49 |
50 | # Shouldn't emit for duplicate field types with same value; covered by Y016
|
= help: Replace with a single `Literal`
PYI030.pyi:51:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 1]`
|
@@ -153,6 +169,7 @@ PYI030.pyi:51:10: PYI030 Multiple literal members in a union. Use a single liter
52 |
53 | # Shouldn't emit if in new parent type
|
= help: Replace with a single `Literal`
PYI030.pyi:60:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -162,6 +179,7 @@ PYI030.pyi:60:10: PYI030 Multiple literal members in a union. Use a single liter
61 |
62 | # Should emit in cases with newlines
|
= help: Replace with a single `Literal`
PYI030.pyi:63:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -177,6 +195,7 @@ PYI030.pyi:63:10: PYI030 Multiple literal members in a union. Use a single liter
69 |
70 | # Should handle multiple unions with multiple members
|
= help: Replace with a single `Literal`
PYI030.pyi:71:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]`
|
@@ -186,6 +205,7 @@ PYI030.pyi:71:10: PYI030 Multiple literal members in a union. Use a single liter
72 |
73 | # Should emit in cases with `typing.Union` instead of `|`
|
= help: Replace with a single `Literal`
PYI030.pyi:74:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -195,6 +215,7 @@ PYI030.pyi:74:10: PYI030 Multiple literal members in a union. Use a single liter
75 |
76 | # Should emit in cases with `typing_extensions.Literal`
|
= help: Replace with a single `Literal`
PYI030.pyi:77:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -204,6 +225,7 @@ PYI030.pyi:77:10: PYI030 Multiple literal members in a union. Use a single liter
78 |
79 | # Should emit in cases with nested `typing.Union`
|
= help: Replace with a single `Literal`
PYI030.pyi:80:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -213,6 +235,7 @@ PYI030.pyi:80:10: PYI030 Multiple literal members in a union. Use a single liter
81 |
82 | # Should emit in cases with mixed `typing.Union` and `|`
|
= help: Replace with a single `Literal`
PYI030.pyi:83:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]`
|
@@ -222,12 +245,24 @@ PYI030.pyi:83:10: PYI030 Multiple literal members in a union. Use a single liter
84 |
85 | # Should emit only once in cases with multiple nested `typing.Union`
|
= help: Replace with a single `Literal`
PYI030.pyi:86:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]`
|
85 | # Should emit only once in cases with multiple nested `typing.Union`
86 | field24: typing.Union[Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]]] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
87 |
88 | # Should use the first literal subscript attribute when fixing
|
= help: Replace with a single `Literal`
PYI030.pyi:89:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]`
|
88 | # Should use the first literal subscript attribute when fixing
89 | field25: typing.Union[typing_extensions.Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]], str] # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
|
= help: Replace with a single `Literal`

View File

@@ -258,7 +258,7 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
if !acc.is_empty() {
acc.push(',');
}
acc.push_str(value.as_str());
acc.push_str(value.to_str());
}
acc
}),
@@ -301,7 +301,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
let names = split_names(value.as_str());
let names = split_names(value.to_str());
if names.len() > 1 {
match names_type {
types::ParametrizeNameType::Tuple => {
@@ -476,7 +476,7 @@ fn check_values(checker: &mut Checker, names: &Expr, values: &Expr) {
.parametrize_values_row_type;
let is_multi_named = if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &names {
split_names(value.as_str()).len() > 1
split_names(value.to_str()).len() > 1
} else {
true
};
@@ -550,7 +550,7 @@ fn check_duplicates(checker: &mut Checker, values: &Expr) {
element.range(),
);
if let Some(prev) = prev {
let values_end = values.range().end() - TextSize::new(1);
let values_end = values.end() - TextSize::new(1);
let previous_end =
trailing_comma(prev, checker.locator().contents()).unwrap_or(values_end);
let element_end =

View File

@@ -161,12 +161,12 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex
return;
}
if is_lowercase_allowed(env_var.as_str()) {
if is_lowercase_allowed(env_var.to_str()) {
return;
}
let capital_env_var = env_var.as_str().to_ascii_uppercase();
if capital_env_var == env_var.as_str() {
let capital_env_var = env_var.to_str().to_ascii_uppercase();
if capital_env_var == env_var.to_str() {
return;
}
@@ -201,12 +201,12 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) {
return;
};
if is_lowercase_allowed(env_var.as_str()) {
if is_lowercase_allowed(env_var.to_str()) {
return;
}
let capital_env_var = env_var.as_str().to_ascii_uppercase();
if capital_env_var == env_var.as_str() {
let capital_env_var = env_var.to_str().to_ascii_uppercase();
if capital_env_var == env_var.to_str() {
return;
}

View File

@@ -16,12 +16,18 @@ use crate::importer::ImportRequest;
///
/// ## Example
/// ```python
/// import trio
///
///
/// async def func():
/// await trio.sleep(0)
/// ```
///
/// Use instead:
/// ```python
/// import trio
///
///
/// async def func():
/// await trio.lowlevel.checkpoint()
/// ```
@@ -103,7 +109,7 @@ pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) {
)?;
let reference_edit =
Edit::range_replacement(format!("{binding}.checkpoint"), call.func.range());
let arg_edit = Edit::range_deletion(call.arguments.range);
let arg_edit = Edit::range_replacement("()".to_string(), call.arguments.range());
Ok(Fix::safe_edits(import_edit, [reference_edit, arg_edit]))
});
checker.diagnostics.push(diagnostic);

View File

@@ -17,7 +17,7 @@ TRIO115.py:5:11: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.s
3 3 | from trio import sleep
4 4 |
5 |- await trio.sleep(0) # TRIO115
5 |+ await trio.lowlevel.checkpoint # TRIO115
5 |+ await trio.lowlevel.checkpoint() # TRIO115
6 6 | await trio.sleep(1) # OK
7 7 | await trio.sleep(0, 1) # OK
8 8 | await trio.sleep(...) # OK
@@ -38,7 +38,7 @@ TRIO115.py:11:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.s
9 9 | await trio.sleep() # OK
10 10 |
11 |- trio.sleep(0) # TRIO115
11 |+ trio.lowlevel.checkpoint # TRIO115
11 |+ trio.lowlevel.checkpoint() # TRIO115
12 12 | foo = 0
13 13 | trio.sleep(foo) # TRIO115
14 14 | trio.sleep(1) # OK
@@ -59,7 +59,7 @@ TRIO115.py:13:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.s
11 11 | trio.sleep(0) # TRIO115
12 12 | foo = 0
13 |- trio.sleep(foo) # TRIO115
13 |+ trio.lowlevel.checkpoint # TRIO115
13 |+ trio.lowlevel.checkpoint() # TRIO115
14 14 | trio.sleep(1) # OK
15 15 | time.sleep(0) # OK
16 16 |
@@ -80,15 +80,15 @@ TRIO115.py:17:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.s
15 15 | time.sleep(0) # OK
16 16 |
17 |- sleep(0) # TRIO115
17 |+ trio.lowlevel.checkpoint # TRIO115
17 |+ trio.lowlevel.checkpoint() # TRIO115
18 18 |
19 19 | bar = "bar"
20 20 | trio.sleep(bar)
TRIO115.py:30:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
TRIO115.py:31:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
29 | def func():
30 | sleep(0) # TRIO115
30 | def func():
31 | sleep(0) # TRIO115
| ^^^^^^^^ TRIO115
|
= help: Replace with `trio.lowlevel.checkpoint()`
@@ -100,8 +100,36 @@ TRIO115.py:30:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.s
27 |-from trio import Event, sleep
27 |+from trio import Event, sleep, lowlevel
28 28 |
29 29 | def func():
30 |- sleep(0) # TRIO115
30 |+ lowlevel.checkpoint # TRIO115
29 29 |
30 30 | def func():
31 |- sleep(0) # TRIO115
31 |+ lowlevel.checkpoint() # TRIO115
32 32 |
33 33 |
34 34 | async def func():
TRIO115.py:35:11: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
34 | async def func():
35 | await sleep(seconds=0) # TRIO115
| ^^^^^^^^^^^^^^^^ TRIO115
|
= help: Replace with `trio.lowlevel.checkpoint()`
Safe fix
24 24 | trio.run(trio.sleep(0)) # TRIO115
25 25 |
26 26 |
27 |-from trio import Event, sleep
27 |+from trio import Event, sleep, lowlevel
28 28 |
29 29 |
30 30 | def func():
--------------------------------------------------------------------------------
32 32 |
33 33 |
34 34 | async def func():
35 |- await sleep(seconds=0) # TRIO115
35 |+ await lowlevel.checkpoint() # TRIO115

View File

@@ -1,7 +1,7 @@
use ruff_python_ast::call_path::from_qualified_name;
use ruff_python_ast::helpers::{map_callable, map_subscript};
use ruff_python_ast::{self as ast};
use ruff_python_semantic::{Binding, BindingId, BindingKind, ScopeKind, SemanticModel};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::{Binding, BindingId, BindingKind, SemanticModel};
use rustc_hash::FxHashSet;
pub(crate) fn is_valid_runtime_import(binding: &Binding, semantic: &SemanticModel) -> bool {
@@ -18,25 +18,26 @@ pub(crate) fn is_valid_runtime_import(binding: &Binding, semantic: &SemanticMode
}
}
pub(crate) fn runtime_evaluated(
pub(crate) fn runtime_evaluated_class(
class_def: &ast::StmtClassDef,
base_classes: &[String],
decorators: &[String],
semantic: &SemanticModel,
) -> bool {
if !base_classes.is_empty() {
if runtime_evaluated_base_class(base_classes, semantic) {
return true;
}
if runtime_evaluated_base_class(class_def, base_classes, semantic) {
return true;
}
if !decorators.is_empty() {
if runtime_evaluated_decorators(decorators, semantic) {
return true;
}
if runtime_evaluated_decorators(class_def, decorators, semantic) {
return true;
}
false
}
fn runtime_evaluated_base_class(base_classes: &[String], semantic: &SemanticModel) -> bool {
fn runtime_evaluated_base_class(
class_def: &ast::StmtClassDef,
base_classes: &[String],
semantic: &SemanticModel,
) -> bool {
fn inner(
class_def: &ast::StmtClassDef,
base_classes: &[String],
@@ -78,19 +79,21 @@ fn runtime_evaluated_base_class(base_classes: &[String], semantic: &SemanticMode
})
}
semantic
.current_scope()
.kind
.as_class()
.is_some_and(|class_def| {
inner(class_def, base_classes, semantic, &mut FxHashSet::default())
})
if base_classes.is_empty() {
return false;
}
inner(class_def, base_classes, semantic, &mut FxHashSet::default())
}
fn runtime_evaluated_decorators(decorators: &[String], semantic: &SemanticModel) -> bool {
let ScopeKind::Class(class_def) = &semantic.current_scope().kind else {
fn runtime_evaluated_decorators(
class_def: &ast::StmtClassDef,
decorators: &[String],
semantic: &SemanticModel,
) -> bool {
if decorators.is_empty() {
return false;
};
}
class_def.decorator_list.iter().any(|decorator| {
semantic
@@ -102,3 +105,72 @@ fn runtime_evaluated_decorators(decorators: &[String], semantic: &SemanticModel)
})
})
}
/// Returns `true` if a function is registered as a `singledispatch` interface.
///
/// For example, `fun` below is a `singledispatch` interface:
/// ```python
/// from functools import singledispatch
///
/// @singledispatch
/// def fun(arg, verbose=False):
/// ...
/// ```
pub(crate) fn is_singledispatch_interface(
function_def: &ast::StmtFunctionDef,
semantic: &SemanticModel,
) -> bool {
function_def.decorator_list.iter().any(|decorator| {
semantic
.resolve_call_path(&decorator.expression)
.is_some_and(|call_path| {
matches!(call_path.as_slice(), ["functools", "singledispatch"])
})
})
}
/// Returns `true` if a function is registered as a `singledispatch` implementation.
///
/// For example, `_` below is a `singledispatch` implementation:
/// For example:
/// ```python
/// from functools import singledispatch
///
/// @singledispatch
/// def fun(arg, verbose=False):
/// ...
///
/// @fun.register
/// def _(arg: int, verbose=False):
/// ...
/// ```
pub(crate) fn is_singledispatch_implementation(
function_def: &ast::StmtFunctionDef,
semantic: &SemanticModel,
) -> bool {
function_def.decorator_list.iter().any(|decorator| {
let Expr::Attribute(attribute) = &decorator.expression else {
return false;
};
if attribute.attr.as_str() != "register" {
return false;
};
let Some(id) = semantic.lookup_attribute(attribute.value.as_ref()) else {
return false;
};
let binding = semantic.binding(id);
let Some(function_def) = binding
.kind
.as_function_definition()
.map(|id| &semantic.scopes[*id])
.and_then(|scope| scope.kind.as_function())
else {
return false;
};
is_singledispatch_interface(function_def, semantic)
})
}

View File

@@ -37,6 +37,7 @@ mod tests {
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("snapshot.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("singledispatch.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("typing_modules_1.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("typing_modules_2.py"))]

View File

@@ -0,0 +1,27 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
singledispatch.py:10:20: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
8 | from numpy.typing import ArrayLike
9 | from scipy.sparse import spmatrix
10 | from pandas import DataFrame
| ^^^^^^^^^ TCH002
11 |
12 | if TYPE_CHECKING:
|
= help: Move into type-checking block
Unsafe fix
7 7 | from numpy import asarray
8 8 | from numpy.typing import ArrayLike
9 9 | from scipy.sparse import spmatrix
10 |-from pandas import DataFrame
11 10 |
12 11 | if TYPE_CHECKING:
12 |+ from pandas import DataFrame
13 13 | from numpy import ndarray
14 14 |
15 15 |

View File

@@ -69,7 +69,7 @@ pub(crate) fn path_constructor_current_directory(checker: &mut Checker, expr: &E
return;
};
if matches!(value.as_str(), "" | ".") {
if matches!(value.to_str(), "" | ".") {
let mut diagnostic = Diagnostic::new(PathConstructorCurrentDirectory, *range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(*range)));
checker.diagnostics.push(diagnostic);

View File

@@ -705,8 +705,6 @@ impl Violation for OsReadlink {
/// ## Examples
/// ```python
/// import os
///
/// import os
/// from pwd import getpwuid
/// from grp import getgrgid
///
@@ -719,8 +717,6 @@ impl Violation for OsReadlink {
/// ```python
/// from pathlib import Path
///
/// from pathlib import Path
///
/// file_path = Path(file_name)
/// stat = file_path.stat()
/// owner_name = file_path.owner()

View File

@@ -67,7 +67,7 @@ fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option<Expr> {
.iter()
.filter_map(|expr| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
Some(value.as_str())
Some(value.to_str())
} else {
None
}

View File

@@ -1138,4 +1138,47 @@ mod tests {
assert_messages!(diagnostics);
Ok(())
}
#[test_case(Path::new("length_sort_straight_imports.py"))]
#[test_case(Path::new("length_sort_from_imports.py"))]
#[test_case(Path::new("length_sort_straight_and_from_imports.py"))]
#[test_case(Path::new("length_sort_non_ascii_members.py"))]
#[test_case(Path::new("length_sort_non_ascii_modules.py"))]
#[test_case(Path::new("length_sort_with_relative_imports.py"))]
fn length_sort(path: &Path) -> Result<()> {
let snapshot = format!("length_sort__{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("isort").join(path).as_path(),
&LinterSettings {
isort: super::settings::Settings {
length_sort: true,
..super::settings::Settings::default()
},
src: vec![test_resource_path("fixtures/isort")],
..LinterSettings::for_rule(Rule::UnsortedImports)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("length_sort_straight_imports.py"))]
#[test_case(Path::new("length_sort_from_imports.py"))]
#[test_case(Path::new("length_sort_straight_and_from_imports.py"))]
fn length_sort_straight(path: &Path) -> Result<()> {
let snapshot = format!("length_sort_straight__{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("isort").join(path).as_path(),
&LinterSettings {
isort: super::settings::Settings {
length_sort_straight: true,
..super::settings::Settings::default()
},
src: vec![test_resource_path("fixtures/isort")],
..LinterSettings::for_rule(Rule::UnsortedImports)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -1,3 +1,4 @@
use crate::rules::isort::sorting::ImportStyle;
use itertools::Itertools;
use super::settings::Settings;
@@ -56,21 +57,34 @@ pub(crate) fn order_imports<'a>(
.map(Import)
.chain(from_imports.map(ImportFrom))
.sorted_by_cached_key(|import| match import {
Import((alias, _)) => {
ModuleKey::from_module(Some(alias.name), alias.asname, None, None, settings)
}
Import((alias, _)) => ModuleKey::from_module(
Some(alias.name),
alias.asname,
None,
None,
ImportStyle::Straight,
settings,
),
ImportFrom((import_from, _, _, aliases)) => ModuleKey::from_module(
import_from.module,
None,
import_from.level,
aliases.first().map(|(alias, _)| (alias.name, alias.asname)),
ImportStyle::From,
settings,
),
})
.collect()
} else {
let ordered_straight_imports = straight_imports.sorted_by_cached_key(|(alias, _)| {
ModuleKey::from_module(Some(alias.name), alias.asname, None, None, settings)
ModuleKey::from_module(
Some(alias.name),
alias.asname,
None,
None,
ImportStyle::Straight,
settings,
)
});
let ordered_from_imports =
from_imports.sorted_by_cached_key(|(import_from, _, _, aliases)| {
@@ -79,6 +93,7 @@ pub(crate) fn order_imports<'a>(
None,
import_from.level,
aliases.first().map(|(alias, _)| (alias.name, alias.asname)),
ImportStyle::From,
settings,
)
});

View File

@@ -58,6 +58,8 @@ pub struct Settings {
pub section_order: Vec<ImportSection>,
pub no_sections: bool,
pub from_first: bool,
pub length_sort: bool,
pub length_sort_straight: bool,
}
impl Default for Settings {
@@ -86,6 +88,8 @@ impl Default for Settings {
section_order: ImportType::iter().map(ImportSection::Known).collect(),
no_sections: false,
from_first: false,
length_sort: false,
length_sort_straight: false,
}
}
}

View File

@@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
length_sort_from_imports.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from mediuuuuuuuuuuum import a
2 | | from short import b
3 | | from loooooooooooooooooooooog import c
|
= help: Organize imports
Safe fix
1 |+from short import b
1 2 | from mediuuuuuuuuuuum import a
2 |-from short import b
3 3 | from loooooooooooooooooooooog import c

View File

@@ -0,0 +1,37 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
length_sort_non_ascii_members.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from module1 import (
2 | | loooooooooooooong,
3 | | σηορτ,
4 | | mediuuuuum,
5 | | shoort,
6 | | looooooooooooooong,
7 | | μεδιυυυυυμ,
8 | | short,
9 | | mediuuuuuum,
10 | | λοοοοοοοοοοοοοονγ,
11 | | )
|
= help: Organize imports
Safe fix
1 1 | from module1 import (
2 |- loooooooooooooong,
2 |+ short,
3 3 | σηορτ,
4 |+ shoort,
4 5 | mediuuuuum,
5 |- shoort,
6 |- looooooooooooooong,
7 6 | μεδιυυυυυμ,
8 |- short,
9 7 | mediuuuuuum,
8 |+ loooooooooooooong,
10 9 | λοοοοοοοοοοοοοονγ,
10 |+ looooooooooooooong,
11 11 | )

View File

@@ -0,0 +1,32 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
length_sort_non_ascii_modules.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / import loooooooooooooong
2 | | import mediuuuuuum
3 | | import short
4 | | import σηορτ
5 | | import shoort
6 | | import mediuuuuum
7 | | import λοοοοοοοοοοοοοονγ
8 | | import μεδιυυυυυμ
9 | | import looooooooooooooong
|
= help: Organize imports
Safe fix
1 |-import loooooooooooooong
2 |-import mediuuuuuum
3 1 | import short
4 2 | import σηορτ
5 3 | import shoort
6 4 | import mediuuuuum
5 |+import μεδιυυυυυμ
6 |+import mediuuuuuum
7 |+import loooooooooooooong
7 8 | import λοοοοοοοοοοοοοονγ
8 |-import μεδιυυυυυμ
9 9 | import looooooooooooooong

View File

@@ -0,0 +1,26 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
length_sort_straight_and_from_imports.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / import mediuuuuuum
2 | | import short
3 | | import looooooooooooooooong
4 | | from looooooooooooooong import a
5 | | from mediuuuum import c
6 | | from short import b
|
= help: Organize imports
Safe fix
1 |+import short
1 2 | import mediuuuuuum
2 |-import short
3 3 | import looooooooooooooooong
4 |-from looooooooooooooong import a
4 |+from short import b
5 5 | from mediuuuum import c
6 |-from short import b
6 |+from looooooooooooooong import a

View File

@@ -0,0 +1,21 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
length_sort_straight_imports.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / import mediuuuuuumb
2 | | import short
3 | | import looooooooooooooooong
4 | | import mediuuuuuuma
|
= help: Organize imports
Safe fix
1 |+import short
2 |+import mediuuuuuuma
1 3 | import mediuuuuuumb
2 |-import short
3 4 | import looooooooooooooooong
4 |-import mediuuuuuuma

View File

@@ -0,0 +1,28 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
length_sort_with_relative_imports.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from ..looooooooooooooong import a
2 | | from ...mediuuuum import b
3 | | from .short import c
4 | | from ....short import c
5 | | from . import d
6 | | from .mediuuuum import a
7 | | from ......short import b
|
= help: Organize imports
Safe fix
1 |-from ..looooooooooooooong import a
2 |-from ...mediuuuum import b
1 |+from . import d
3 2 | from .short import c
4 3 | from ....short import c
5 |-from . import d
6 4 | from .mediuuuum import a
7 5 | from ......short import b
6 |+from ...mediuuuum import b
7 |+from ..looooooooooooooong import a

View File

@@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
length_sort_from_imports.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from mediuuuuuuuuuuum import a
2 | | from short import b
3 | | from loooooooooooooooooooooog import c
|
= help: Organize imports
Safe fix
1 |+from loooooooooooooooooooooog import c
1 2 | from mediuuuuuuuuuuum import a
2 3 | from short import b
3 |-from loooooooooooooooooooooog import c

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
length_sort_straight_and_from_imports.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / import mediuuuuuum
2 | | import short
3 | | import looooooooooooooooong
4 | | from looooooooooooooong import a
5 | | from mediuuuum import c
6 | | from short import b
|
= help: Organize imports
Safe fix
1 |+import short
1 2 | import mediuuuuuum
2 |-import short
3 3 | import looooooooooooooooong
4 4 | from looooooooooooooong import a
5 5 | from mediuuuum import c

View File

@@ -0,0 +1,21 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
length_sort_straight_imports.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / import mediuuuuuumb
2 | | import short
3 | | import looooooooooooooooong
4 | | import mediuuuuuuma
|
= help: Organize imports
Safe fix
1 |+import short
2 |+import mediuuuuuuma
1 3 | import mediuuuuuumb
2 |-import short
3 4 | import looooooooooooooooong
4 |-import mediuuuuuuma

View File

@@ -3,6 +3,7 @@
use std::{borrow::Cow, cmp::Ordering, cmp::Reverse};
use natord;
use unicode_width::UnicodeWidthStr;
use ruff_python_stdlib::str;
@@ -64,18 +65,27 @@ impl<'a> From<String> for NatOrdStr<'a> {
}
}
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)]
pub(crate) enum Distance {
Nearest(u32),
Furthest(Reverse<u32>),
}
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)]
pub(crate) enum ImportStyle {
// Ex) `import foo`
Straight,
// Ex) `from foo import bar`
From,
}
/// A comparable key to capture the desired sorting order for an imported module (e.g.,
/// `foo` in `from foo import bar`).
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
pub(crate) struct ModuleKey<'a> {
force_to_top: bool,
maybe_length: Option<usize>,
distance: Distance,
force_to_top: Option<bool>,
maybe_lowercase_name: Option<NatOrdStr<'a>>,
module_name: Option<NatOrdStr<'a>>,
first_alias: Option<MemberKey<'a>>,
@@ -88,26 +98,39 @@ impl<'a> ModuleKey<'a> {
asname: Option<&'a str>,
level: Option<u32>,
first_alias: Option<(&'a str, Option<&'a str>)>,
style: ImportStyle,
settings: &Settings,
) -> Self {
let level = level.unwrap_or_default();
let force_to_top = !name
.map(|name| settings.force_to_top.contains(name))
.unwrap_or_default(); // `false` < `true` so we get forced to top first
let maybe_length = (settings.length_sort
|| (settings.length_sort_straight && style == ImportStyle::Straight))
.then_some(name.map(str::width).unwrap_or_default() + level as usize);
let distance = match settings.relative_imports_order {
RelativeImportsOrder::ClosestToFurthest => Distance::Nearest(level.unwrap_or_default()),
RelativeImportsOrder::FurthestToClosest => {
Distance::Furthest(Reverse(level.unwrap_or_default()))
}
RelativeImportsOrder::ClosestToFurthest => Distance::Nearest(level),
RelativeImportsOrder::FurthestToClosest => Distance::Furthest(Reverse(level)),
};
let force_to_top = name.map(|name| !settings.force_to_top.contains(name)); // `false` < `true` so we get forced to top first
let maybe_lowercase_name = name.and_then(|name| {
(!settings.case_sensitive).then_some(NatOrdStr(maybe_lowercase(name)))
});
let module_name = name.map(NatOrdStr::from);
let asname = asname.map(NatOrdStr::from);
let first_alias =
first_alias.map(|(name, asname)| MemberKey::from_member(name, asname, settings));
Self {
distance,
force_to_top,
maybe_length,
distance,
maybe_lowercase_name,
module_name,
first_alias,
@@ -122,6 +145,7 @@ impl<'a> ModuleKey<'a> {
pub(crate) struct MemberKey<'a> {
not_star_import: bool,
member_type: Option<MemberType>,
maybe_length: Option<usize>,
maybe_lowercase_name: Option<NatOrdStr<'a>>,
module_name: NatOrdStr<'a>,
asname: Option<NatOrdStr<'a>>,
@@ -133,6 +157,7 @@ impl<'a> MemberKey<'a> {
let member_type = settings
.order_by_type
.then_some(member_type(name, settings));
let maybe_length = settings.length_sort.then_some(name.width());
let maybe_lowercase_name =
(!settings.case_sensitive).then_some(NatOrdStr(maybe_lowercase(name)));
let module_name = NatOrdStr::from(name);
@@ -141,6 +166,7 @@ impl<'a> MemberKey<'a> {
Self {
not_star_import,
member_type,
maybe_length,
maybe_lowercase_name,
module_name,
asname,

View File

@@ -1,4 +1,5 @@
use itertools::Itertools;
use ruff_python_ast::call_path::collect_call_path;
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
use ruff_python_semantic::SemanticModel;
@@ -72,6 +73,7 @@ pub(super) fn is_type_alias_assignment(stmt: &Stmt, semantic: &SemanticModel) ->
}
}
/// Returns `true` if the statement is an assignment to a `TypedDict`.
pub(super) fn is_typed_dict_class(arguments: Option<&Arguments>, semantic: &SemanticModel) -> bool {
arguments.is_some_and(|arguments| {
arguments
@@ -81,6 +83,67 @@ pub(super) fn is_typed_dict_class(arguments: Option<&Arguments>, semantic: &Sema
})
}
/// Returns `true` if a statement appears to be a dynamic import of a Django model.
///
/// For example, in Django, it's common to use `get_model` to access a model dynamically, as in:
/// ```python
/// def migrate_existing_attachment_data(
/// apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
/// ) -> None:
/// Attachment = apps.get_model("zerver", "Attachment")
/// ```
pub(super) fn is_django_model_import(name: &str, stmt: &Stmt, semantic: &SemanticModel) -> bool {
fn match_model_import(name: &str, expr: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Call(ast::ExprCall {
func, arguments, ..
}) = expr
else {
return false;
};
// Match against, e.g., `apps.get_model("zerver", "Attachment")`.
if let Some(call_path) = collect_call_path(func.as_ref()) {
if matches!(call_path.as_slice(), [.., "get_model"]) {
if let Some(argument) =
arguments.find_argument("model_name", arguments.args.len() - 1)
{
if let Some(string_literal) = argument.as_string_literal_expr() {
return string_literal.value.to_str() == name;
}
}
}
}
// Match against, e.g., `import_string("zerver.models.Attachment")`.
if let Some(call_path) = semantic.resolve_call_path(func.as_ref()) {
if matches!(
call_path.as_slice(),
["django", "utils", "module_loading", "import_string"]
) {
if let Some(argument) = arguments.find_argument("dotted_path", 0) {
if let Some(string_literal) = argument.as_string_literal_expr() {
if let Some((.., model)) = string_literal.value.to_str().rsplit_once('.') {
return model == name;
}
}
}
}
}
false
}
match stmt {
Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value), ..
}) => match_model_import(name, value.as_ref(), semantic),
Stmt::Assign(ast::StmtAssign { value, .. }) => {
match_model_import(name, value.as_ref(), semantic)
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::{is_acronym, is_camelcase, is_mixed_case};

View File

@@ -53,6 +53,15 @@ impl Violation for NonLowercaseVariableInFunction {
/// N806
pub(crate) fn non_lowercase_variable_in_function(checker: &mut Checker, expr: &Expr, name: &str) {
// Ignore globals.
if checker
.semantic()
.lookup_symbol(name)
.is_some_and(|id| checker.semantic().binding(id).is_global())
{
return;
}
if checker
.settings
.pep8_naming
@@ -72,6 +81,7 @@ pub(crate) fn non_lowercase_variable_in_function(checker: &mut Checker, expr: &E
|| helpers::is_typed_dict_assignment(parent, checker.semantic())
|| helpers::is_type_var_assignment(parent, checker.semantic())
|| helpers::is_type_alias_assignment(parent, checker.semantic())
|| helpers::is_django_model_import(name, parent, checker.semantic())
{
return;
}

View File

@@ -20,4 +20,22 @@ N806.py:13:5: N806 Variable `CONSTANT` in function should be lowercase
14 | _ = 0
|
N806.py:46:5: N806 Variable `Bad` in function should be lowercase
|
45 | def model_assign() -> None:
46 | Bad = apps.get_model("zerver", "Stream") # N806
| ^^^ N806
47 | Attachment = apps.get_model("zerver", "Attachment") # OK
48 | Recipient = apps.get_model("zerver", model_name="Recipient") # OK
|
N806.py:53:5: N806 Variable `Bad` in function should be lowercase
|
51 | from django.utils.module_loading import import_string
52 |
53 | Bad = import_string("django.core.exceptions.ValidationError") # N806
| ^^^ N806
54 | ValidationError = import_string("django.core.exceptions.ValidationError") # OK
|

View File

@@ -37,6 +37,7 @@ mod tests {
#[test_case(Rule::MixedSpacesAndTabs, Path::new("E101.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E40.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.ipynb"))]
#[test_case(Rule::MultipleImportsOnOneLine, Path::new("E40.py"))]
#[test_case(Rule::MultipleStatementsOnOneLineColon, Path::new("E70.py"))]
#[test_case(Rule::MultipleStatementsOnOneLineSemicolon, Path::new("E70.py"))]

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