Compare commits

...

22 Commits

Author SHA1 Message Date
Charlie Marsh
f460f9c5c0 Bump version to v0.1.6 (#8744) 2023-11-17 13:29:19 -05:00
Tuomas Siipola
2faac1e7a8 [refurb] Implement math-constant (FURB152) (#8727)
## Summary

Implements
[FURB152](https://github.com/dosisod/refurb/blob/master/docs/checks.md#furb152-use-math-constant)
that checks for literals that are similar to constants in `math` module,
for example:

```python
A = 3.141592 * r ** 2
```

Use instead:
```python
A = math.pi * r ** 2
```

Related to #1348.
2023-11-17 17:37:44 +00:00
Charlie Marsh
b7dbb9062c Remove incorrect deprecation label for stdout and stderr (#8743)
Closes https://github.com/astral-sh/ruff/issues/8738.
2023-11-17 12:34:02 -05:00
Charlie Marsh
66794bc9fe Remove erroneous bad-dunder-name reference (#8742)
Closes #8731.
2023-11-17 17:26:29 +00:00
konsti
dca430f4d2 Fix instability with await fluent style (#8676)
Fix an instability where await was followed by a breaking fluent style
expression:

```python
test_data = await (
    Stream.from_async(async_data)
    .flat_map_async()
    .map()
    .filter_async(is_valid_data)
    .to_list()
)
```

Note that this technically a minor style change (see ecosystem check)
2023-11-17 12:24:19 -05:00
Adil Zouitine
841e6c889e Add River in "Who's Using Ruff?" section (#8740)
## Summary

I added [`River`](https://github.com/online-ml/river) in "Who's Using
Ruff?" section. River is an online machine learning library, with 4.5k
stars and 460k downloads, since few months we used
[`Ruff`](3758eb1e0a/pyproject.toml (L31))
as linter.
2023-11-17 08:13:50 -06:00
Zanie Blue
bd99175fea Update D208 to preserve indentation offsets when fixing overindented lines (#8699)
Closes https://github.com/astral-sh/ruff/issues/8695

We track the smallest offset seen for overindented lines then only
reduce the indentation of the lines that far to preserve indentation in
other lines. This rule's behavior now matches our formatter, which is
nice.

We may want to gate this with preview.
2023-11-16 22:11:07 -06:00
Ofek Lev
4c86b155f2 Fix typo (#8735) 2023-11-16 22:10:09 -06:00
Vince Chan
e2109c1353 Improve debug printing for resolving origin of config settings (#8729)
## Summary

When running ruff in verbose mode with `-v`, the first debug logs show
where the config settings are taken from. For example:
```
❯ ruff check ./some_file.py -v
[2023-11-17][00:16:25][ruff_cli::resolve][DEBUG] Using pyproject.toml (parent) at /Users/vince/demo/ruff.toml
```

This threw me off for a second because I knew I had no python project
there, and therefore no `pyproject.toml` file. Then I realised it was
actually reading a `ruff.toml` file (obvious when you read the whole
print I suppose) and that the pyproject.toml is a hardcoded string in
the debug log.

I think it would be nice to tweak the wording slightly so it is clear
that the settings don't neccessarily have to come from a
`pyproject.toml` file.
2023-11-17 01:10:36 +00:00
Charlie Marsh
1fcccf82fc Avoid syntax error via importing trio.lowlevel (#8730)
We ended up with a syntax error here via `from trio import
lowlevel.checkpoint`. The new solution avoids that error, but does miss
cases like:

```py
from trio.lowlevel import Timer
```

Where it could insert `from trio.lowlevel import Timer, checkpoint`.
Instead, it'll add `from trio import lowlevel`.

See:
https://github.com/astral-sh/ruff/issues/8402#issuecomment-1810838129
2023-11-17 01:07:59 +00:00
konsti
14e65afdc6 Update to Rust 1.74 and use new clippy lints table (#8722)
Update to [Rust
1.74](https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html) and use
the new clippy lints table.

The update itself introduced a new clippy lint about superfluous hashes
in raw strings, which got removed.

I moved our lint config from `rustflags` to the newly stabilized
[workspace.lints](https://doc.rust-lang.org/stable/cargo/reference/workspaces.html#the-lints-table).
One consequence is that we have to `unsafe_code = "warn"` instead of
"forbid" because the latter now actually bans unsafe code:

```
error[E0453]: allow(unsafe_code) incompatible with previous forbid
  --> crates/ruff_source_file/src/newlines.rs:62:17
   |
62 |         #[allow(unsafe_code)]
   |                 ^^^^^^^^^^^ overruled by previous forbid
   |
   = note: `forbid` lint level was set on command line
```

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-11-16 18:12:46 -05:00
Charlie Marsh
6d5d079a18 Avoid missing namespace violations in scripts with shebangs (#8710)
## Summary

I think it's reasonable to avoid raising `INP001` for scripts, and
shebangs are one sufficient way to detect scripts.

Closes https://github.com/astral-sh/ruff/issues/8690.
2023-11-16 17:21:33 -05:00
Zanie Blue
d1e88dc984 Update UP032 to unescape curly braces in literal parts of converted strings (#8697)
Closes #8694
2023-11-16 13:59:54 -06:00
konsti
dda31b6996 List all ipython builtins (#8719)
I checked for ipython-specific builtins on python 3.11 using
```python
import json
from subprocess import check_output

builtins_python = json.loads(check_output(["python3", "-c" "import json; print(json.dumps(dir(__builtins__)))"]))
builtins_ipython = json.loads(check_output(["ipython3", "-c" "import json; print(json.dumps(dir(__builtins__)))"]))
print(sorted(set(builtins_ipython) - set(builtins_python)))
```
and updated the relevant constant and match. The list changes from

`display`

to

`__IPYTHON__`, `display`, `get_ipython`.

Followup to #8707
2023-11-16 19:06:25 +01:00
Charlie Marsh
b6a7787318 Remove pyproject.toml from fixtures directory (#8726)
## Summary

This exists to power a test, but it ends up affecting the behavior of
all files in the directory. Namely, it means that these files _aren't_
excluded when you format or lint them directly, since in that case, Ruff
will fall back to looking at the `pyproject.toml` in
`crates/ruff_linter/resources/test/fixtures`, which _doesn't_ exclude
these files, unlike our top-level `pyproject.toml`.
2023-11-16 13:04:52 -05:00
Jonas Haag
5fa961f670 Improve N803 example (#8714) 2023-11-16 12:50:31 -05:00
Charlie Marsh
2424188bb2 Trim trailing empty strings when converting to f-strings (#8712)
## Summary

When converting from a `.format` call to an f-string, we can trim any
trailing empty tokens.

Closes https://github.com/astral-sh/ruff/issues/8683.
2023-11-15 23:14:49 -05:00
Charlie Marsh
a59172528c Add fix for future-required-type-annotation (#8711)
## Summary

We already support inserting imports for `I002` -- this PR just adds the
same fix for `FA102`, which is explicitly about `from __future__ import
annotations`.

Closes https://github.com/astral-sh/ruff/issues/8682.
2023-11-16 03:08:02 +00:00
Charlie Marsh
cd29761b9c Run unicode prefix rule over tokens (#8709)
## Summary

It seems like the range of an `ExprStringLiteral` can be somewhat
unreliable when the string is part of an implicit concatenation with an
f-string. Using the tokens themselves is more reliable.

Closes #8680.
Closes https://github.com/astral-sh/ruff/issues/7784.
2023-11-16 02:30:42 +00:00
Charlie Marsh
4ac78d5725 Treat display as a builtin in IPython (#8707)
## Summary

`display` is a special-cased builtin in IPython. This PR adds it to the
builtin namespace when analyzing IPython notebooks.

Closes https://github.com/astral-sh/ruff/issues/8702.
2023-11-16 01:58:44 +00:00
Alan Du
2083352ae3 Add autofix for PIE800 (#8668)
## Summary

This adds an autofix for PIE800 (unnecessary spread) -- whenever we see
a `**{...}` inside another dictionary literal, just delete the `**{` and
`}` to inline the key-value pairs. So `{"a": "b", **{"c": "d"}}` becomes
just `{"a": "b", "c": "d"}`.

I have enabled this just for preview mode.

## Test Plan

Updated the preview snapshot test.
2023-11-15 18:11:04 +00:00
Tuomas Siipola
0e2ece5217 Implement FURB136 (#8664)
## Summary

Implements
[FURB136](https://github.com/dosisod/refurb/blob/master/docs/checks.md#furb136-use-min-max)
that checks for `if` expressions that can be replaced with `min()` or
`max()` calls. See issue #1348 for more information.

This implementation diverges from Refurb's original implementation by
retaining the order of equal values. For example, Refurb suggest that
the following expressions:

```python
highest_score1 = score1 if score1 > score2 else score2
highest_score2 = score1 if score1 >= score2 else score2
```

should be to rewritten as:

```python
highest_score1 = max(score1, score2)
highest_score2 = max(score1, score2)
```

whereas this implementation provides more correct alternatives:

```python
highest_score1 = max(score2, score1)
highest_score2 = max(score1, score2)
```

## Test Plan

Unit test checks all eight possibilities.
2023-11-15 18:10:13 +00:00
153 changed files with 3467 additions and 1360 deletions

View File

@@ -1,37 +1,3 @@
[alias]
dev = "run --package ruff_dev --bin ruff_dev"
benchmark = "bench -p ruff_benchmark --bench linter --bench formatter --"
[target.'cfg(all())']
rustflags = [
# CLIPPY LINT SETTINGS
# This is a workaround to configure lints for the entire workspace, pending the ability to configure this via TOML.
# See: `https://github.com/rust-lang/cargo/issues/5034`
# `https://github.com/EmbarkStudios/rust-ecosystem/issues/22#issuecomment-947011395`
"-Dunsafe_code",
"-Wclippy::pedantic",
# Allowed pedantic lints
"-Wclippy::char_lit_as_u8",
"-Aclippy::collapsible_else_if",
"-Aclippy::collapsible_if",
"-Aclippy::implicit_hasher",
"-Aclippy::match_same_arms",
"-Aclippy::missing_errors_doc",
"-Aclippy::missing_panics_doc",
"-Aclippy::module_name_repetitions",
"-Aclippy::must_use_candidate",
"-Aclippy::similar_names",
"-Aclippy::too_many_lines",
# Disallowed restriction lints
"-Wclippy::print_stdout",
"-Wclippy::print_stderr",
"-Wclippy::dbg_macro",
"-Wclippy::empty_drop",
"-Wclippy::empty_structs_with_brackets",
"-Wclippy::exit",
"-Wclippy::get_unwrap",
"-Wclippy::rc_buffer",
"-Wclippy::rc_mutex",
"-Wclippy::rest_pat_in_fully_bound_structs",
"-Wunreachable_pub"
]

View File

@@ -1,5 +1,78 @@
# Changelog
## 0.1.6
### Preview features
- \[`flake8-boolean-trap`\] Extend `boolean-type-hint-positional-argument` (`FBT001`) to include booleans in unions ([#7501](https://github.com/astral-sh/ruff/pull/7501))
- \[`flake8-pie`\] Extend `reimplemented-list-builtin` (`PIE807`) to `dict` reimplementations ([#8608](https://github.com/astral-sh/ruff/pull/8608))
- \[`flake8-pie`\] Extend `unnecessary-pass` (`PIE790`) to include ellipses (`...`) ([#8641](https://github.com/astral-sh/ruff/pull/8641))
- \[`flake8-pie`\] Implement fix for `unnecessary-spread` (`PIE800`) ([#8668](https://github.com/astral-sh/ruff/pull/8668))
- \[`flake8-quotes`\] Implement `unnecessary-escaped-quote` (`Q004`) ([#8630](https://github.com/astral-sh/ruff/pull/8630))
- \[`pycodestyle`\] Implement fix for `multiple-spaces-after-keyword` (`E271`) and `multiple-spaces-before-keyword` (`E272`) ([#8622](https://github.com/astral-sh/ruff/pull/8622))
- \[`pycodestyle`\] Implement fix for `multiple-spaces-after-operator` (`E222`) and `multiple-spaces-before-operator` (`E221`) ([#8623](https://github.com/astral-sh/ruff/pull/8623))
- \[`pyflakes`\] Extend `is-literal` (`F632`) to include comparisons against mutable initializers ([#8607](https://github.com/astral-sh/ruff/pull/8607))
- \[`pylint`\] Implement `redefined-argument-from-local` (`PLR1704`) ([#8159](https://github.com/astral-sh/ruff/pull/8159))
- \[`pylint`\] Implement fix for `unnecessary-lambda` (`PLW0108`) ([#8621](https://github.com/astral-sh/ruff/pull/8621))
- \[`refurb`\] Implement `if-expr-min-max` (`FURB136`) ([#8664](https://github.com/astral-sh/ruff/pull/8664))
- \[`refurb`\] Implement `math-constant` (`FURB152`) ([#8727](https://github.com/astral-sh/ruff/pull/8727))
### Rule changes
- \[`flake8-annotations`\] Add autotyping-like return type inference for annotation rules ([#8643](https://github.com/astral-sh/ruff/pull/8643))
- \[`flake8-future-annotations`\] Implement fix for `future-required-type-annotation` (`FA102`) ([#8711](https://github.com/astral-sh/ruff/pull/8711))
- \[`flake8-implicit-namespace-package`\] Avoid missing namespace violations in scripts with shebangs ([#8710](https://github.com/astral-sh/ruff/pull/8710))
- \[`pydocstyle`\] Update `over-indentation` (`D208`) to preserve indentation offsets when fixing overindented lines ([#8699](https://github.com/astral-sh/ruff/pull/8699))
- \[`pyupgrade`\] Refine `timeout-error-alias` (`UP041`) to remove false positives ([#8587](https://github.com/astral-sh/ruff/pull/8587))
### Formatter
- Fix instability in `await` formatting with fluent style ([#8676](https://github.com/astral-sh/ruff/pull/8676))
- Compare formatted and unformatted ASTs during formatter tests ([#8624](https://github.com/astral-sh/ruff/pull/8624))
- Preserve trailing semicolon for Notebooks ([#8590](https://github.com/astral-sh/ruff/pull/8590))
### CLI
- Improve debug printing for resolving origin of config settings ([#8729](https://github.com/astral-sh/ruff/pull/8729))
- Write unchanged, excluded files to stdout when read via stdin ([#8596](https://github.com/astral-sh/ruff/pull/8596))
### Configuration
- \[`isort`\] Support disabling sections with `no-sections = true` ([#8657](https://github.com/astral-sh/ruff/pull/8657))
- \[`pep8-naming`\] Support local and dynamic class- and static-method decorators ([#8592](https://github.com/astral-sh/ruff/pull/8592))
- \[`pydocstyle`\] Allow overriding pydocstyle convention rules ([#8586](https://github.com/astral-sh/ruff/pull/8586))
### Bug fixes
- Avoid syntax error via importing `trio.lowlevel` ([#8730](https://github.com/astral-sh/ruff/pull/8730))
- Omit unrolled augmented assignments in `PIE794` ([#8634](https://github.com/astral-sh/ruff/pull/8634))
- Slice source code instead of generating it for `EM` fixes ([#7746](https://github.com/astral-sh/ruff/pull/7746))
- Allow whitespace around colon in slices for `whitespace-before-punctuation` (`E203`) ([#8654](https://github.com/astral-sh/ruff/pull/8654))
- Use function range for `no-self-use` ([#8637](https://github.com/astral-sh/ruff/pull/8637))
- F-strings doesn't contain bytes literal for `PLW0129` ([#8675](https://github.com/astral-sh/ruff/pull/8675))
- Improve detection of `TYPE_CHECKING` blocks imported from `typing_extensions` or `_typeshed` ([#8429](https://github.com/astral-sh/ruff/pull/8429))
- Treat display as a builtin in IPython ([#8707](https://github.com/astral-sh/ruff/pull/8707))
- Avoid `FURB113` autofix if comments are present ([#8494](https://github.com/astral-sh/ruff/pull/8494))
- Consider the new f-string tokens for `flake8-commas` ([#8582](https://github.com/astral-sh/ruff/pull/8582))
- Remove erroneous bad-dunder-name reference ([#8742](https://github.com/astral-sh/ruff/pull/8742))
- Avoid recommending Self usages in metaclasses ([#8639](https://github.com/astral-sh/ruff/pull/8639))
- Detect runtime-evaluated base classes defined in the current file ([#8572](https://github.com/astral-sh/ruff/pull/8572))
- Avoid inserting trailing commas within f-strings ([#8574](https://github.com/astral-sh/ruff/pull/8574))
- Remove incorrect deprecation label for stdout and stderr ([#8743](https://github.com/astral-sh/ruff/pull/8743))
- Fix unnecessary parentheses in UP007 fix ([#8610](https://github.com/astral-sh/ruff/pull/8610))
- Remove repeated and erroneous scoped settings headers in docs ([#8670](https://github.com/astral-sh/ruff/pull/8670))
- Trim trailing empty strings when converting to f-strings ([#8712](https://github.com/astral-sh/ruff/pull/8712))
- Fix ordering for `force-sort-within-sections` ([#8665](https://github.com/astral-sh/ruff/pull/8665))
- Run unicode prefix rule over tokens ([#8709](https://github.com/astral-sh/ruff/pull/8709))
- Update UP032 to unescape curly braces in literal parts of converted strings ([#8697](https://github.com/astral-sh/ruff/pull/8697))
- List all ipython builtins ([#8719](https://github.com/astral-sh/ruff/pull/8719))
### Documentation
- Document conventions in the FAQ ([#8638](https://github.com/astral-sh/ruff/pull/8638))
- Redirect from rule codes to rule pages in docs ([#8636](https://github.com/astral-sh/ruff/pull/8636))
- Fix permalink to convention setting ([#8575](https://github.com/astral-sh/ruff/pull/8575))
## 0.1.5
### Preview features

8
Cargo.lock generated
View File

@@ -808,7 +808,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"anyhow",
"clap",
@@ -2062,7 +2062,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"annotate-snippets 0.9.2",
"anyhow",
@@ -2198,7 +2198,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2449,7 +2449,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"anyhow",
"clap",

View File

@@ -55,6 +55,38 @@ unicode-width = { version = "0.1.11" }
uuid = { version = "1.5.0", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
[workspace.lints.rust]
unsafe_code = "warn"
unreachable_pub = "warn"
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -2 }
# Allowed pedantic lints
char_lit_as_u8 = "allow"
collapsible_else_if = "allow"
collapsible_if = "allow"
implicit_hasher = "allow"
match_same_arms = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
module_name_repetitions = "allow"
must_use_candidate = "allow"
similar_names = "allow"
too_many_lines = "allow"
# To allow `#[allow(clippy::all)]` in `crates/ruff_python_parser/src/python.rs`.
needless_raw_string_hashes = "allow"
# Disallowed restriction lints
print_stdout = "warn"
print_stderr = "warn"
dbg_macro = "warn"
empty_drop = "warn"
empty_structs_with_brackets = "warn"
exit = "warn"
get_unwrap = "warn"
rc_buffer = "warn"
rc_mutex = "warn"
rest_pat_in_fully_bound_structs = "warn"
[profile.release]
lto = "fat"
codegen-units = 1

View File

@@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.5
rev: v0.1.6
hooks:
# Run the linter.
- id: ruff
@@ -428,6 +428,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Pydantic](https://github.com/pydantic/pydantic)
- [Pylint](https://github.com/PyCQA/pylint)
- [Reflex](https://github.com/reflex-dev/reflex)
- [River](https://github.com/online-ml/river)
- [Rippling](https://rippling.com)
- [Robyn](https://github.com/sansyrox/robyn)
- [Saleor](https://github.com/saleor/saleor)

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.1.5"
version = "0.1.6"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""
@@ -34,3 +34,6 @@ toml = { workspace = true }
[dev-dependencies]
pretty_assertions = "1.3.0"
[lints]
workspace = true

View File

@@ -46,6 +46,9 @@ ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_python_index = { path = "../ruff_python_index" }
ruff_python_parser = { path = "../ruff_python_parser" }
[lints]
workspace = true
[features]
codspeed = ["codspeed-criterion-compat"]

View File

@@ -20,3 +20,6 @@ seahash = "4.1.0"
[dev-dependencies]
ruff_macros = { path = "../ruff_macros" }
[lints]
workspace = true

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.1.5"
version = "0.1.6"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -76,3 +76,6 @@ mimalloc = "0.1.39"
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies]
tikv-jemallocator = "0.5.0"
[lints]
workspace = true

View File

@@ -202,12 +202,12 @@ fn lint_path(
match result {
Ok(inner) => inner,
Err(error) => {
let message = r#"This indicates a bug in Ruff. If you could open an issue at:
let message = r"This indicates a bug in Ruff. If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
"#;
";
error!(
"{}{}{} {message}\n{error}",

View File

@@ -660,12 +660,12 @@ impl Display for FormatCommandError {
}
}
Self::Panic(path, err) => {
let message = r#"This indicates a bug in Ruff. If you could open an issue at:
let message = r"This indicates a bug in Ruff. If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BFormatter%20panic%5D
...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
"#;
";
if let Some(path) = path {
write!(
f,

View File

@@ -63,7 +63,7 @@ fn format_rule_text(rule: Rule) -> String {
if rule.is_preview() || rule.is_nursery() {
output.push_str(
r#"This rule is in preview and is not stable. The `--preview` flag is required for use."#,
r"This rule is in preview and is not stable. The `--preview` flag is required for use.",
);
output.push('\n');
output.push('\n');

View File

@@ -43,7 +43,7 @@ pub fn resolve(
{
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, overrides)?;
debug!(
"Using user specified pyproject.toml at {}",
"Using user-specified configuration file at: {}",
pyproject.display()
);
return Ok(PyprojectConfig::new(
@@ -63,7 +63,10 @@ pub fn resolve(
.as_ref()
.unwrap_or(&path_dedot::CWD.as_path()),
)? {
debug!("Using pyproject.toml (parent) at {}", pyproject.display());
debug!(
"Using configuration file (via parent) at: {}",
pyproject.display()
);
let settings = resolve_root_settings(&pyproject, Relativity::Parent, overrides)?;
return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical,
@@ -77,7 +80,10 @@ pub fn resolve(
// end up the "closest" `pyproject.toml` file for every Python file later on, so
// these act as the "default" settings.)
if let Some(pyproject) = pyproject::find_user_settings_toml() {
debug!("Using pyproject.toml (cwd) at {}", pyproject.display());
debug!(
"Using configuration file (via cwd) at: {}",
pyproject.display()
);
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, overrides)?;
return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical,

View File

@@ -395,9 +395,9 @@ fn deprecated_options() -> Result<()> {
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
r"
tab-size = 2
"#,
",
)?;
insta::with_settings!({filters => vec![
@@ -407,10 +407,10 @@ tab-size = 2
.args(["format", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
.pass_stdin(r"
if True:
pass
"#), @r###"
"), @r###"
success: true
exit_code: 0
----- stdout -----
@@ -443,9 +443,9 @@ format = "json"
.args(["check", "--select", "F401", "--no-cache", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
.pass_stdin(r"
import os
"#), @r###"
"), @r###"
success: false
exit_code: 2
----- stdout -----

View File

@@ -48,9 +48,12 @@ tracing-indicatif = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
imara-diff = "0.1.5"
[dev-dependencies]
indoc = "2.0.4"
[features]
# Turn off rayon for profiling
singlethreaded = []
[dev-dependencies]
indoc = "2.0.4"
[lints]
workspace = true

View File

@@ -49,7 +49,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
if rule.is_preview() || rule.is_nursery() {
output.push_str(
r#"This rule is unstable and in [preview](../preview.md). The `--preview` flag is required for use."#,
r"This rule is unstable and in [preview](../preview.md). The `--preview` flag is required for use.",
);
output.push('\n');
output.push('\n');

View File

@@ -29,3 +29,6 @@ insta = { workspace = true }
[features]
serde = ["dep:serde", "ruff_text_size/serde"]
schemars = ["dep:schemars", "ruff_text_size/schemars"]
[lints]
workspace = true

View File

@@ -1711,14 +1711,14 @@ mod tests {
));
assert_eq!(
r#"a
"a
b
c
d
d
c
b
a"#,
a",
formatted.as_code()
);
}
@@ -2047,10 +2047,10 @@ two lines`,
assert_eq!(
printed.as_code(),
r#"Group with id-2
"Group with id-2
Group with id-1 does not fit on the line because it exceeds the line width of 80 characters by
Group 2 fits
Group 1 breaks"#
Group 1 breaks"
);
}

View File

@@ -17,3 +17,6 @@ ruff_macros = { path = "../ruff_macros" }
[dev-dependencies]
static_assertions = "1.1.0"
[lints]
workspace = true

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.1.5"
version = "0.1.6"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -86,3 +86,6 @@ default = []
schemars = ["dep:schemars"]
# Enables the UnreachableCode rule
unreachable-code = []
[lints]
workspace = true

View File

@@ -1,3 +0,0 @@
# fixtures
Fixture files used for snapshot testing.

View File

@@ -1,9 +1,21 @@
{"foo": 1, **{"bar": 1}} # PIE800
{**{"bar": 10}, "a": "b"} # PIE800
foo({**foo, **{"bar": True}}) # PIE800
{**foo, **{"bar": 10}} # PIE800
{ # PIE800
"a": "b",
# Preserve
**{
# all
"bar": 10, # the
# comments
},
}
{**foo, **buzz, **{bar: 10}} # PIE800
{**foo, "bar": True } # OK

View File

@@ -1,8 +1,7 @@
import trio
from trio import sleep
async def func():
import trio
from trio import sleep
await trio.sleep(0) # TRIO115
await trio.sleep(1) # OK
await trio.sleep(0, 1) # OK
@@ -21,8 +20,11 @@ async def func():
trio.sleep(bar)
trio.sleep(0) # TRIO115
def func():
trio.run(trio.sleep(0)) # TRIO115
from trio import Event, sleep
def func():
sleep(0) # TRIO115

View File

@@ -663,3 +663,55 @@ class CommentAfterDocstring:
def newline_after_closing_quote(self):
"We enforce a newline after the closing quote for a multi-line docstring \
but continuations shouldn't be considered multi-line"
def retain_extra_whitespace():
"""Summary.
This is overindented
And so is this, but it we should preserve the extra space on this line relative
to the one before
"""
def retain_extra_whitespace_multiple():
"""Summary.
This is overindented
And so is this, but it we should preserve the extra space on this line relative
to the one before
This is also overindented
And so is this, but it we should preserve the extra space on this line relative
to the one before
"""
def retain_extra_whitespace_deeper():
"""Summary.
This is overindented
And so is this, but it we should preserve the extra space on this line relative
to the one before
And the relative indent here should be preserved too
"""
def retain_extra_whitespace_followed_by_same_offset():
"""Summary.
This is overindented
And so is this, but it we should preserve the extra space on this line relative
This is overindented
This is overindented
"""
def retain_extra_whitespace_not_overindented():
"""Summary.
This is not overindented
This is overindented, but since one line is not overindented this should not raise
And so is this, but it we should preserve the extra space on this line relative
"""

View File

@@ -0,0 +1,4 @@
"""Test for IPython-only builtins."""
x = 1
display(x)

View File

@@ -0,0 +1,74 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "af8fee97-f9aa-47c2-b34c-b109a5f083d6",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"1"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"1"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "acd5eb1e-6991-42b8-806f-20d17d7e571f",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"1"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"display(1)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f8f3f599-030e-48b3-bcd3-31d97f725c68",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"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.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -1,10 +0,0 @@
[tool.ruff]
line-length = 88
extend-exclude = [
"excluded_file.py",
"migrations",
"with_excluded_file/other_excluded_file.py",
]
[tool.ruff.lint]
per-file-ignores = { "__init__.py" = ["F401"] }

View File

@@ -25,3 +25,6 @@ u = u
def hello():
return"Hello"
f"foo"u"bar"
f"foo" u"bar"

View File

@@ -207,3 +207,39 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
# The fixed string will exceed the line length, but it's still smaller than the
# existing line length, so it's fine.
"<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
# When fixing, trim the trailing empty string.
raise ValueError("Conflicting configuration dicts: {!r} {!r}"
"".format(new_dict, d))
# When fixing, trim the trailing empty string.
raise ValueError("Conflicting configuration dicts: {!r} {!r}"
.format(new_dict, d))
raise ValueError(
"Conflicting configuration dicts: {!r} {!r}"
"".format(new_dict, d)
)
raise ValueError(
"Conflicting configuration dicts: {!r} {!r}"
"".format(new_dict, d)
)
# The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped
(
"{}"
"{{}}"
).format(a)
("{}" "{{}}").format(a)
# Both strings will be converted to an f-string and the curly braces in the second should left escaped
(
"{}"
"{{{}}}"
).format(a, b)
("{}" "{{{}}}").format(a, b)

View File

@@ -0,0 +1,25 @@
x = 1
y = 2
x if x > y else y # FURB136
x if x >= y else y # FURB136
x if x < y else y # FURB136
x if x <= y else y # FURB136
y if x > y else x # FURB136
y if x >= y else x # FURB136
y if x < y else x # FURB136
y if x <= y else x # FURB136
x + y if x > y else y # OK
x if (
x
> y
) else y # FURB136

View File

@@ -0,0 +1,7 @@
r = 3.1 # OK
A = 3.14 * r ** 2 # FURB152
C = 6.28 * r # FURB152
e = 2.71 # FURB152

View File

@@ -936,13 +936,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_trio::rules::zero_sleep_call(checker, call);
}
}
Expr::Dict(
dict @ ast::ExprDict {
keys,
values,
range: _,
},
) => {
Expr::Dict(dict) => {
if checker.any_enabled(&[
Rule::MultiValueRepeatedKeyLiteral,
Rule::MultiValueRepeatedKeyVariable,
@@ -950,7 +944,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyflakes::rules::repeated_keys(checker, dict);
}
if checker.enabled(Rule::UnnecessarySpread) {
flake8_pie::rules::unnecessary_spread(checker, keys, values);
flake8_pie::rules::unnecessary_spread(checker, dict);
}
}
Expr::Set(ast::ExprSet { elts, range: _ }) => {
@@ -1251,10 +1245,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
refurb::rules::single_item_membership_test(checker, expr, left, ops, comparators);
}
}
Expr::NumberLiteral(_) => {
Expr::NumberLiteral(number_literal @ ast::ExprNumberLiteral { .. }) => {
if checker.source_type.is_stub() && checker.enabled(Rule::NumericLiteralTooLong) {
flake8_pyi::rules::numeric_literal_too_long(checker, expr);
}
if checker.enabled(Rule::MathConstant) {
refurb::rules::math_constant(checker, number_literal);
}
}
Expr::BytesLiteral(_) => {
if checker.source_type.is_stub() && checker.enabled(Rule::StringOrBytesTooLong) {
@@ -1272,21 +1269,20 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::HardcodedTempFile) {
flake8_bandit::rules::hardcoded_tmp_directory(checker, string);
}
if checker.enabled(Rule::UnicodeKindPrefix) {
pyupgrade::rules::unicode_kind_prefix(checker, string);
}
if checker.source_type.is_stub() {
if checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
}
}
}
Expr::IfExp(ast::ExprIfExp {
test,
body,
orelse,
range: _,
}) => {
Expr::IfExp(
if_exp @ ast::ExprIfExp {
test,
body,
orelse,
range: _,
},
) => {
if checker.enabled(Rule::IfElseBlockInsteadOfDictGet) {
flake8_simplify::rules::if_exp_instead_of_dict_get(
checker, expr, test, body, orelse,
@@ -1301,6 +1297,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::IfExprWithTwistedArms) {
flake8_simplify::rules::twisted_arms_in_ifexpr(checker, expr, test, body, orelse);
}
if checker.enabled(Rule::IfExprMinMax) {
refurb::rules::if_expr_min_max(checker, if_exp);
}
}
Expr::ListComp(
comp @ ast::ExprListComp {

View File

@@ -54,7 +54,7 @@ use ruff_python_semantic::{
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, Snapshot,
StarImport, SubmoduleImport,
};
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python_stdlib::builtins::{IPYTHON_BUILTINS, MAGIC_GLOBALS, PYTHON_BUILTINS};
use ruff_source_file::Locator;
use crate::checkers::ast::deferred::Deferred;
@@ -1592,9 +1592,16 @@ impl<'a> Checker<'a> {
}
fn bind_builtins(&mut self) {
for builtin in BUILTINS
for builtin in PYTHON_BUILTINS
.iter()
.chain(MAGIC_GLOBALS.iter())
.chain(
self.source_type
.is_ipynb()
.then_some(IPYTHON_BUILTINS)
.into_iter()
.flatten(),
)
.copied()
.chain(self.settings.builtins.iter().map(String::as_str))
{

View File

@@ -1,6 +1,8 @@
use std::path::Path;
use ruff_diagnostics::Diagnostic;
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use crate::registry::Rule;
use crate::rules::flake8_no_pep420::rules::implicit_namespace_package;
@@ -10,15 +12,22 @@ use crate::settings::LinterSettings;
pub(crate) fn check_file_path(
path: &Path,
package: Option<&Path>,
locator: &Locator,
indexer: &Indexer,
settings: &LinterSettings,
) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![];
// flake8-no-pep420
if settings.rules.enabled(Rule::ImplicitNamespacePackage) {
if let Some(diagnostic) =
implicit_namespace_package(path, package, &settings.project_root, &settings.src)
{
if let Some(diagnostic) = implicit_namespace_package(
path,
package,
locator,
indexer,
&settings.project_root,
&settings.src,
) {
diagnostics.push(diagnostic);
}
}

View File

@@ -91,6 +91,10 @@ pub(crate) fn check_tokens(
pycodestyle::rules::tab_indentation(&mut diagnostics, tokens, locator, indexer);
}
if settings.rules.enabled(Rule::UnicodeKindPrefix) {
pyupgrade::rules::unicode_kind_prefix(&mut diagnostics, tokens);
}
if settings.rules.any_enabled(&[
Rule::InvalidCharacterBackspace,
Rule::InvalidCharacterSub,
@@ -163,7 +167,7 @@ pub(crate) fn check_tokens(
Rule::ShebangNotFirstLine,
Rule::ShebangMissingPython,
]) {
flake8_executable::rules::from_tokens(tokens, path, locator, &mut diagnostics);
flake8_executable::rules::from_tokens(&mut diagnostics, path, locator, indexer);
}
if settings.rules.any_enabled(&[

View File

@@ -947,9 +947,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
#[allow(deprecated)]
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
(Refurb, "136") => (RuleGroup::Preview, rules::refurb::rules::IfExprMinMax),
(Refurb, "140") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedStarmap),
(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, "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

@@ -171,7 +171,7 @@ mod tests {
#[test]
fn empty_file() {
let locator = Locator::new(r#""#);
let locator = Locator::new(r"");
let diagnostics = create_diagnostics([]);
let FixResult {
code,
@@ -225,10 +225,10 @@ print("hello world")
#[test]
fn apply_one_replacement() {
let locator = Locator::new(
r#"
r"
class A(object):
...
"#
"
.trim(),
);
let diagnostics = create_diagnostics([Edit::replacement(
@@ -243,10 +243,10 @@ class A(object):
} = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(
code,
r#"
r"
class A(Bar):
...
"#
"
.trim(),
);
assert_eq!(fixes.values().sum::<usize>(), 1);
@@ -262,10 +262,10 @@ class A(Bar):
#[test]
fn apply_one_removal() {
let locator = Locator::new(
r#"
r"
class A(object):
...
"#
"
.trim(),
);
let diagnostics = create_diagnostics([Edit::deletion(TextSize::new(7), TextSize::new(15))]);
@@ -276,10 +276,10 @@ class A(object):
} = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(
code,
r#"
r"
class A:
...
"#
"
.trim()
);
assert_eq!(fixes.values().sum::<usize>(), 1);
@@ -295,10 +295,10 @@ class A:
#[test]
fn apply_two_removals() {
let locator = Locator::new(
r#"
r"
class A(object, object, object):
...
"#
"
.trim(),
);
let diagnostics = create_diagnostics([
@@ -313,10 +313,10 @@ class A(object, object, object):
assert_eq!(
code,
r#"
r"
class A(object):
...
"#
"
.trim()
);
assert_eq!(fixes.values().sum::<usize>(), 2);
@@ -334,10 +334,10 @@ class A(object):
#[test]
fn ignore_overlapping_fixes() {
let locator = Locator::new(
r#"
r"
class A(object):
...
"#
"
.trim(),
);
let diagnostics = create_diagnostics([
@@ -351,10 +351,10 @@ class A(object):
} = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(
code,
r#"
r"
class A:
...
"#
"
.trim(),
);
assert_eq!(fixes.values().sum::<usize>(), 1);

View File

@@ -373,18 +373,18 @@ mod tests {
Insertion::own_line("", TextSize::from(40), "\n")
);
let contents = r#"
let contents = r"
x = 1
"#
"
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(0), "\n")
);
let contents = r#"
let contents = r"
#!/usr/bin/env python3
"#
"
.trim_start();
assert_eq!(
insert(contents)?,
@@ -457,10 +457,10 @@ x = 1
Insertion::inline("", TextSize::from(9), "; ")
);
let contents = r#"
let contents = r"
if True:
pass
"#
"
.trim_start();
assert_eq!(
insert(contents, TextSize::from(0)),

View File

@@ -7,7 +7,7 @@ use std::error::Error;
use anyhow::Result;
use libcst_native::{ImportAlias, Name, NameOrAttribute};
use ruff_python_ast::{self as ast, PySourceType, Stmt, Suite};
use ruff_python_ast::{self as ast, PySourceType, Stmt};
use ruff_text_size::{Ranged, TextSize};
use ruff_diagnostics::Edit;
@@ -26,7 +26,7 @@ mod insertion;
pub(crate) struct Importer<'a> {
/// The Python AST to which we are adding imports.
python_ast: &'a Suite,
python_ast: &'a [Stmt],
/// The [`Locator`] for the Python AST.
locator: &'a Locator<'a>,
/// The [`Stylist`] for the Python AST.
@@ -39,7 +39,7 @@ pub(crate) struct Importer<'a> {
impl<'a> Importer<'a> {
pub(crate) fn new(
python_ast: &'a Suite,
python_ast: &'a [Stmt],
locator: &'a Locator<'a>,
stylist: &'a Stylist<'a>,
) -> Self {

View File

@@ -117,7 +117,7 @@ pub fn check_path(
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_filesystem())
{
diagnostics.extend(check_file_path(path, package, settings));
diagnostics.extend(check_file_path(path, package, locator, indexer, settings));
}
// Run the logical line-based rules.
@@ -639,7 +639,7 @@ mod tests {
use crate::registry::Rule;
use crate::source_kind::SourceKind;
use crate::test::{test_contents, test_notebook_path, TestedNotebook};
use crate::test::{assert_notebook_path, test_contents, TestedNotebook};
use crate::{assert_messages, settings};
/// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory.
@@ -655,7 +655,7 @@ mod tests {
messages,
source_notebook,
..
} = test_notebook_path(
} = assert_notebook_path(
&actual,
expected,
&settings::LinterSettings::for_rule(Rule::UnsortedImports),
@@ -672,7 +672,7 @@ mod tests {
messages,
source_notebook,
..
} = test_notebook_path(
} = assert_notebook_path(
&actual,
expected,
&settings::LinterSettings::for_rule(Rule::UnusedImport),
@@ -689,7 +689,7 @@ mod tests {
messages,
source_notebook,
..
} = test_notebook_path(
} = assert_notebook_path(
&actual,
expected,
&settings::LinterSettings::for_rule(Rule::UnusedVariable),
@@ -706,7 +706,7 @@ mod tests {
let TestedNotebook {
linted_notebook: fixed_notebook,
..
} = test_notebook_path(
} = assert_notebook_path(
actual_path,
&expected_path,
&settings::LinterSettings::for_rule(Rule::UnusedImport),

View File

@@ -199,7 +199,7 @@ def fibonacci(n):
TextSize::from(99),
)));
let file_2 = r#"if a == 1: pass"#;
let file_2 = r"if a == 1: pass";
let undefined_name = Diagnostic::new(
DiagnosticKind {

View File

@@ -297,6 +297,7 @@ impl Rule {
| Rule::TabIndentation
| Rule::TrailingCommaOnBareTuple
| Rule::TypeCommentInStub
| Rule::UnicodeKindPrefix
| Rule::UselessSemicolon
| Rule::UTF8EncodingDeclaration => LintSource::Tokens,
Rule::IOError => LintSource::Io,

View File

@@ -1,5 +1,14 @@
use ruff_python_stdlib::builtins::is_builtin;
use ruff_python_ast::PySourceType;
use ruff_python_stdlib::builtins::{is_ipython_builtin, is_python_builtin};
pub(super) fn shadows_builtin(name: &str, ignorelist: &[String]) -> bool {
is_builtin(name) && ignorelist.iter().all(|ignore| ignore != name)
pub(super) fn shadows_builtin(
name: &str,
ignorelist: &[String],
source_type: PySourceType,
) -> bool {
if is_python_builtin(name) || source_type.is_ipynb() && is_ipython_builtin(name) {
ignorelist.iter().all(|ignore| ignore != name)
} else {
false
}
}

View File

@@ -67,6 +67,7 @@ pub(crate) fn builtin_argument_shadowing(checker: &mut Checker, parameter: &Para
if shadows_builtin(
parameter.name.as_str(),
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
) {
checker.diagnostics.push(Diagnostic::new(
BuiltinArgumentShadowing {

View File

@@ -74,7 +74,11 @@ pub(crate) fn builtin_attribute_shadowing(
name: &str,
range: TextRange,
) {
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
) {
// Ignore shadowing within `TypedDict` definitions, since these are only accessible through
// subscripting and not through attribute access.
if class_def
@@ -102,7 +106,11 @@ pub(crate) fn builtin_method_shadowing(
decorator_list: &[Decorator],
range: TextRange,
) {
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
) {
// Ignore some standard-library methods. Ideally, we'd ignore all overridden methods, since
// those should be flagged on the superclass, but that's more difficult.
if is_standard_library_override(name, class_def, checker.semantic()) {

View File

@@ -60,7 +60,11 @@ impl Violation for BuiltinVariableShadowing {
/// A001
pub(crate) fn builtin_variable_shadowing(checker: &mut Checker, name: &str, range: TextRange) {
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
) {
checker.diagnostics.push(Diagnostic::new(
BuiltinVariableShadowing {
name: name.to_string(),

View File

@@ -12,11 +12,11 @@ mod tests {
#[test]
fn notice() {
let diagnostics = test_snippet(
r#"
r"
# Copyright 2023
import os
"#
"
.trim(),
&settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]),
);
@@ -26,11 +26,11 @@ import os
#[test]
fn notice_with_c() {
let diagnostics = test_snippet(
r#"
r"
# Copyright (C) 2023
import os
"#
"
.trim(),
&settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]),
);
@@ -40,11 +40,11 @@ import os
#[test]
fn notice_with_caps() {
let diagnostics = test_snippet(
r#"
r"
# COPYRIGHT (C) 2023
import os
"#
"
.trim(),
&settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]),
);
@@ -54,11 +54,11 @@ import os
#[test]
fn notice_with_range() {
let diagnostics = test_snippet(
r#"
r"
# Copyright (C) 2021-2023
import os
"#
"
.trim(),
&settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]),
);
@@ -68,11 +68,11 @@ import os
#[test]
fn valid_author() {
let diagnostics = test_snippet(
r#"
r"
# Copyright (C) 2023 Ruff
import os
"#
"
.trim(),
&settings::LinterSettings {
flake8_copyright: super::settings::Settings {
@@ -88,11 +88,11 @@ import os
#[test]
fn invalid_author() {
let diagnostics = test_snippet(
r#"
r"
# Copyright (C) 2023 Some Author
import os
"#
"
.trim(),
&settings::LinterSettings {
flake8_copyright: super::settings::Settings {
@@ -108,9 +108,9 @@ import os
#[test]
fn small_file() {
let diagnostics = test_snippet(
r#"
r"
import os
"#
"
.trim(),
&settings::LinterSettings {
flake8_copyright: super::settings::Settings {
@@ -126,7 +126,7 @@ import os
#[test]
fn late_notice() {
let diagnostics = test_snippet(
r#"
r"
# Content Content Content Content Content Content Content Content Content Content
# Content Content Content Content Content Content Content Content Content Content
# Content Content Content Content Content Content Content Content Content Content
@@ -149,7 +149,7 @@ import os
# Content Content Content Content Content Content Content Content Content Content
# Copyright 2023
"#
"
.trim(),
&settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]),
);
@@ -159,8 +159,8 @@ import os
#[test]
fn char_boundary() {
let diagnostics = test_snippet(
r#"কককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককক
"#
r"কককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককক
"
.trim(),
&settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]),
);

View File

@@ -1,9 +1,7 @@
use std::path::Path;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_diagnostics::Diagnostic;
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
pub(crate) use shebang_leading_whitespace::*;
pub(crate) use shebang_missing_executable_file::*;
@@ -20,32 +18,31 @@ mod shebang_not_executable;
mod shebang_not_first_line;
pub(crate) fn from_tokens(
tokens: &[LexResult],
diagnostics: &mut Vec<Diagnostic>,
path: &Path,
locator: &Locator,
diagnostics: &mut Vec<Diagnostic>,
indexer: &Indexer,
) {
let mut has_any_shebang = false;
for (tok, range) in tokens.iter().flatten() {
if let Tok::Comment(comment) = tok {
if let Some(shebang) = ShebangDirective::try_extract(comment) {
has_any_shebang = true;
for range in indexer.comment_ranges() {
let comment = locator.slice(*range);
if let Some(shebang) = ShebangDirective::try_extract(comment) {
has_any_shebang = true;
if let Some(diagnostic) = shebang_missing_python(*range, &shebang) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_missing_python(*range, &shebang) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_not_executable(path, *range) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_not_executable(path, *range) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_leading_whitespace(*range, locator) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_leading_whitespace(*range, locator) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_not_first_line(*range, locator) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_not_first_line(*range, locator) {
diagnostics.push(diagnostic);
}
}
}

View File

@@ -1,11 +1,13 @@
use ruff_python_ast::Expr;
use std::fmt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
use ruff_python_ast::imports::{AnyImport, ImportFrom};
use ruff_python_ast::Expr;
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
use crate::importer::Importer;
/// ## What it does
/// Checks for uses of PEP 585- and PEP 604-style type annotations in Python
@@ -42,6 +44,10 @@ use crate::checkers::ast::Checker;
/// ...
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as adding `from __future__ import annotations`
/// may change the semantics of the program.
///
/// ## Options
/// - `target-version`
#[violation]
@@ -66,18 +72,28 @@ impl fmt::Display for Reason {
}
}
impl Violation for FutureRequiredTypeAnnotation {
impl AlwaysFixableViolation for FutureRequiredTypeAnnotation {
#[derive_message_formats]
fn message(&self) -> String {
let FutureRequiredTypeAnnotation { reason } = self;
format!("Missing `from __future__ import annotations`, but uses {reason}")
}
fn fix_title(&self) -> String {
format!("Add `from __future__ import annotations`")
}
}
/// FA102
pub(crate) fn future_required_type_annotation(checker: &mut Checker, expr: &Expr, reason: Reason) {
checker.diagnostics.push(Diagnostic::new(
FutureRequiredTypeAnnotation { reason },
expr.range(),
));
let mut diagnostic = Diagnostic::new(FutureRequiredTypeAnnotation { reason }, expr.range());
if let Some(python_ast) = checker.semantic().definitions.python_ast() {
let required_import =
AnyImport::ImportFrom(ImportFrom::member("__future__", "annotations"));
diagnostic.set_fix(Fix::unsafe_edit(
Importer::new(python_ast, checker.locator(), checker.stylist())
.add_import(&required_import, TextSize::default()),
));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,19 +1,33 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
---
no_future_import_uses_lowercase.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
no_future_import_uses_lowercase.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
1 | def main() -> None:
2 | a_list: list[str] = []
| ^^^^^^^^^ FA102
3 | a_list.append("hello")
|
= help: Add `from __future__ import annotations`
no_future_import_uses_lowercase.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
Unsafe fix
1 |+from __future__ import annotations
1 2 | def main() -> None:
2 3 | a_list: list[str] = []
3 4 | a_list.append("hello")
no_future_import_uses_lowercase.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
6 | def hello(y: dict[str, int]) -> None:
| ^^^^^^^^^^^^^^ FA102
7 | del y
|
= help: Add `from __future__ import annotations`
Unsafe fix
1 |+from __future__ import annotations
1 2 | def main() -> None:
2 3 | a_list: list[str] = []
3 4 | a_list.append("hello")

View File

@@ -1,34 +1,62 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
---
no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
no_future_import_uses_union.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
1 | def main() -> None:
2 | a_list: list[str] | None = []
| ^^^^^^^^^ FA102
3 | a_list.append("hello")
|
= help: Add `from __future__ import annotations`
no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
Unsafe fix
1 |+from __future__ import annotations
1 2 | def main() -> None:
2 3 | a_list: list[str] | None = []
3 4 | a_list.append("hello")
no_future_import_uses_union.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
1 | def main() -> None:
2 | a_list: list[str] | None = []
| ^^^^^^^^^^^^^^^^ FA102
3 | a_list.append("hello")
|
= help: Add `from __future__ import annotations`
no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
Unsafe fix
1 |+from __future__ import annotations
1 2 | def main() -> None:
2 3 | a_list: list[str] | None = []
3 4 | a_list.append("hello")
no_future_import_uses_union.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
6 | def hello(y: dict[str, int] | None) -> None:
| ^^^^^^^^^^^^^^ FA102
7 | del y
|
= help: Add `from __future__ import annotations`
no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
Unsafe fix
1 |+from __future__ import annotations
1 2 | def main() -> None:
2 3 | a_list: list[str] | None = []
3 4 | a_list.append("hello")
no_future_import_uses_union.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
6 | def hello(y: dict[str, int] | None) -> None:
| ^^^^^^^^^^^^^^^^^^^^^ FA102
7 | del y
|
= help: Add `from __future__ import annotations`
Unsafe fix
1 |+from __future__ import annotations
1 2 | def main() -> None:
2 3 | a_list: list[str] | None = []
3 4 | a_list.append("hello")

View File

@@ -1,52 +1,94 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
---
no_future_import_uses_union_inner.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
no_future_import_uses_union_inner.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
1 | def main() -> None:
2 | a_list: list[str | None] = []
| ^^^^^^^^^^^^^^^^ FA102
3 | a_list.append("hello")
|
= help: Add `from __future__ import annotations`
no_future_import_uses_union_inner.py:2:18: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
Unsafe fix
1 |+from __future__ import annotations
1 2 | def main() -> None:
2 3 | a_list: list[str | None] = []
3 4 | a_list.append("hello")
no_future_import_uses_union_inner.py:2:18: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
1 | def main() -> None:
2 | a_list: list[str | None] = []
| ^^^^^^^^^^ FA102
3 | a_list.append("hello")
|
= help: Add `from __future__ import annotations`
no_future_import_uses_union_inner.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
Unsafe fix
1 |+from __future__ import annotations
1 2 | def main() -> None:
2 3 | a_list: list[str | None] = []
3 4 | a_list.append("hello")
no_future_import_uses_union_inner.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
6 | def hello(y: dict[str | None, int]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^ FA102
7 | z: tuple[str, str | None, str] = tuple(y)
8 | del z
|
= help: Add `from __future__ import annotations`
no_future_import_uses_union_inner.py:6:19: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
Unsafe fix
1 |+from __future__ import annotations
1 2 | def main() -> None:
2 3 | a_list: list[str | None] = []
3 4 | a_list.append("hello")
no_future_import_uses_union_inner.py:6:19: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
6 | def hello(y: dict[str | None, int]) -> None:
| ^^^^^^^^^^ FA102
7 | z: tuple[str, str | None, str] = tuple(y)
8 | del z
|
= help: Add `from __future__ import annotations`
no_future_import_uses_union_inner.py:7:8: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
Unsafe fix
1 |+from __future__ import annotations
1 2 | def main() -> None:
2 3 | a_list: list[str | None] = []
3 4 | a_list.append("hello")
no_future_import_uses_union_inner.py:7:8: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
6 | def hello(y: dict[str | None, int]) -> None:
7 | z: tuple[str, str | None, str] = tuple(y)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA102
8 | del z
|
= help: Add `from __future__ import annotations`
no_future_import_uses_union_inner.py:7:19: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
Unsafe fix
1 |+from __future__ import annotations
1 2 | def main() -> None:
2 3 | a_list: list[str | None] = []
3 4 | a_list.append("hello")
no_future_import_uses_union_inner.py:7:19: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
6 | def hello(y: dict[str | None, int]) -> None:
7 | z: tuple[str, str | None, str] = tuple(y)
| ^^^^^^^^^^ FA102
8 | del z
|
= help: Add `from __future__ import annotations`
Unsafe fix
1 |+from __future__ import annotations
1 2 | def main() -> None:
2 3 | a_list: list[str | None] = []
3 4 | a_list.append("hello")

View File

@@ -16,7 +16,7 @@ mod tests {
#[test_case(Path::new("test_pass_init"), Path::new("example.py"))]
#[test_case(Path::new("test_fail_empty"), Path::new("example.py"))]
#[test_case(Path::new("test_fail_nonempty"), Path::new("example.py"))]
#[test_case(Path::new("test_fail_shebang"), Path::new("example.py"))]
#[test_case(Path::new("test_pass_shebang"), Path::new("example.py"))]
#[test_case(Path::new("test_ignored"), Path::new("example.py"))]
#[test_case(Path::new("test_pass_namespace_package"), Path::new("example.py"))]
#[test_case(Path::new("test_pass_pyi"), Path::new("example.pyi"))]

View File

@@ -1,10 +1,12 @@
use std::path::{Path, PathBuf};
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use ruff_text_size::{TextRange, TextSize};
use crate::comments::shebang::ShebangDirective;
use crate::fs;
/// ## What it does
@@ -42,6 +44,8 @@ impl Violation for ImplicitNamespacePackage {
pub(crate) fn implicit_namespace_package(
path: &Path,
package: Option<&Path>,
locator: &Locator,
indexer: &Indexer,
project_root: &Path,
src: &[PathBuf],
) -> Option<Diagnostic> {
@@ -56,6 +60,11 @@ pub(crate) fn implicit_namespace_package(
&& !path
.parent()
.is_some_and( |parent| src.iter().any(|src| src == parent))
// Ignore files that contain a shebang.
&& !indexer
.comment_ranges()
.first().filter(|range| range.start() == TextSize::from(0))
.is_some_and(|range| ShebangDirective::try_extract(locator.slice(*range)).is_some())
{
#[cfg(all(test, windows))]
let path = path

View File

@@ -1,11 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_no_pep420/mod.rs
---
example.py:1:1: INP001 File `./resources/test/fixtures/flake8_no_pep420/test_fail_shebang/example.py` is part of an implicit namespace package. Add an `__init__.py`.
|
1 | #!/bin/env/python
| INP001
2 | print('hi')
|

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_no_pep420/mod.rs
---

View File

@@ -32,6 +32,7 @@ mod tests {
}
#[test_case(Rule::UnnecessaryPlaceholder, Path::new("PIE790.py"))]
#[test_case(Rule::UnnecessarySpread, Path::new("PIE800.py"))]
#[test_case(Rule::ReimplementedContainerBuiltin, Path::new("PIE807.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(

View File

@@ -1,9 +1,10 @@
use ruff_python_ast::Expr;
use ruff_python_ast::{self as ast, Expr};
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
@@ -32,22 +33,76 @@ use crate::checkers::ast::Checker;
pub struct UnnecessarySpread;
impl Violation for UnnecessarySpread {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary spread `**`")
}
fn fix_title(&self) -> Option<String> {
Some(format!("Remove unnecessary dict"))
}
}
/// PIE800
pub(crate) fn unnecessary_spread(checker: &mut Checker, keys: &[Option<Expr>], values: &[Expr]) {
for item in keys.iter().zip(values.iter()) {
pub(crate) fn unnecessary_spread(checker: &mut Checker, dict: &ast::ExprDict) {
// The first "end" is the start of the dictionary, immediately following the open bracket.
let mut prev_end = dict.start() + TextSize::from(1);
for item in dict.keys.iter().zip(dict.values.iter()) {
if let (None, value) = item {
// We only care about when the key is None which indicates a spread `**`
// inside a dict.
if let Expr::Dict(_) = value {
let diagnostic = Diagnostic::new(UnnecessarySpread, value.range());
if let Expr::Dict(inner) = value {
let mut diagnostic = Diagnostic::new(UnnecessarySpread, value.range());
if checker.settings.preview.is_enabled() {
if let Some(fix) = unnecessary_spread_fix(inner, prev_end, checker.locator()) {
diagnostic.set_fix(fix);
}
}
checker.diagnostics.push(diagnostic);
}
}
prev_end = item.1.end();
}
}
/// Generate a [`Fix`] to remove an unnecessary dictionary spread.
fn unnecessary_spread_fix(
dict: &ast::ExprDict,
prev_end: TextSize,
locator: &Locator,
) -> Option<Fix> {
// Find the `**` token preceding the spread.
let doublestar = SimpleTokenizer::starts_at(prev_end, locator.contents())
.find(|tok| matches!(tok.kind(), SimpleTokenKind::DoubleStar))?;
if let Some(last) = dict.values.last() {
// Ex) `**{a: 1, b: 2}`
let mut edits = vec![];
for tok in SimpleTokenizer::starts_at(last.end(), locator.contents()).skip_trivia() {
match tok.kind() {
SimpleTokenKind::Comma => {
edits.push(Edit::range_deletion(tok.range()));
}
SimpleTokenKind::RBrace => {
edits.push(Edit::range_deletion(tok.range()));
break;
}
_ => {}
}
}
Some(Fix::safe_edits(
// Delete the first `**{`
Edit::deletion(doublestar.start(), dict.start() + TextSize::from(1)),
// Delete the trailing `}`
edits,
))
} else {
// Ex) `**{}`
Some(Fix::safe_edit(Edit::deletion(
doublestar.start(),
dict.end(),
)))
}
}

View File

@@ -6,37 +6,67 @@ PIE800.py:1:14: PIE800 Unnecessary spread `**`
1 | {"foo": 1, **{"bar": 1}} # PIE800
| ^^^^^^^^^^ PIE800
2 |
3 | foo({**foo, **{"bar": True}}) # PIE800
3 | {**{"bar": 10}, "a": "b"} # PIE800
|
= help: Remove unnecessary dict
PIE800.py:3:15: PIE800 Unnecessary spread `**`
PIE800.py:3:4: PIE800 Unnecessary spread `**`
|
1 | {"foo": 1, **{"bar": 1}} # PIE800
2 |
3 | foo({**foo, **{"bar": True}}) # PIE800
3 | {**{"bar": 10}, "a": "b"} # PIE800
| ^^^^^^^^^^^ PIE800
4 |
5 | foo({**foo, **{"bar": True}}) # PIE800
|
= help: Remove unnecessary dict
PIE800.py:5:15: PIE800 Unnecessary spread `**`
|
3 | {**{"bar": 10}, "a": "b"} # PIE800
4 |
5 | foo({**foo, **{"bar": True}}) # PIE800
| ^^^^^^^^^^^^^ PIE800
4 |
5 | {**foo, **{"bar": 10}} # PIE800
6 |
7 | {**foo, **{"bar": 10}} # PIE800
|
= help: Remove unnecessary dict
PIE800.py:5:11: PIE800 Unnecessary spread `**`
PIE800.py:7:11: PIE800 Unnecessary spread `**`
|
3 | foo({**foo, **{"bar": True}}) # PIE800
4 |
5 | {**foo, **{"bar": 10}} # PIE800
5 | foo({**foo, **{"bar": True}}) # PIE800
6 |
7 | {**foo, **{"bar": 10}} # PIE800
| ^^^^^^^^^^^ PIE800
6 |
7 | {**foo, **buzz, **{bar: 10}} # PIE800
|
PIE800.py:7:19: PIE800 Unnecessary spread `**`
|
5 | {**foo, **{"bar": 10}} # PIE800
6 |
7 | {**foo, **buzz, **{bar: 10}} # PIE800
| ^^^^^^^^^ PIE800
8 |
9 | {**foo, "bar": True } # OK
9 | { # PIE800
|
= help: Remove unnecessary dict
PIE800.py:12:7: PIE800 Unnecessary spread `**`
|
10 | "a": "b",
11 | # Preserve
12 | **{
| _______^
13 | | # all
14 | | "bar": 10, # the
15 | | # comments
16 | | },
| |_____^ PIE800
17 | }
|
= help: Remove unnecessary dict
PIE800.py:19:19: PIE800 Unnecessary spread `**`
|
17 | }
18 |
19 | {**foo, **buzz, **{bar: 10}} # PIE800
| ^^^^^^^^^ PIE800
20 |
21 | {**foo, "bar": True } # OK
|
= help: Remove unnecessary dict

View File

@@ -0,0 +1,134 @@
---
source: crates/ruff_linter/src/rules/flake8_pie/mod.rs
---
PIE800.py:1:14: PIE800 [*] Unnecessary spread `**`
|
1 | {"foo": 1, **{"bar": 1}} # PIE800
| ^^^^^^^^^^ PIE800
2 |
3 | {**{"bar": 10}, "a": "b"} # PIE800
|
= help: Remove unnecessary dict
Safe fix
1 |-{"foo": 1, **{"bar": 1}} # PIE800
1 |+{"foo": 1, "bar": 1} # PIE800
2 2 |
3 3 | {**{"bar": 10}, "a": "b"} # PIE800
4 4 |
PIE800.py:3:4: PIE800 [*] Unnecessary spread `**`
|
1 | {"foo": 1, **{"bar": 1}} # PIE800
2 |
3 | {**{"bar": 10}, "a": "b"} # PIE800
| ^^^^^^^^^^^ PIE800
4 |
5 | foo({**foo, **{"bar": True}}) # PIE800
|
= help: Remove unnecessary dict
Safe fix
1 1 | {"foo": 1, **{"bar": 1}} # PIE800
2 2 |
3 |-{**{"bar": 10}, "a": "b"} # PIE800
3 |+{"bar": 10, "a": "b"} # PIE800
4 4 |
5 5 | foo({**foo, **{"bar": True}}) # PIE800
6 6 |
PIE800.py:5:15: PIE800 [*] Unnecessary spread `**`
|
3 | {**{"bar": 10}, "a": "b"} # PIE800
4 |
5 | foo({**foo, **{"bar": True}}) # PIE800
| ^^^^^^^^^^^^^ PIE800
6 |
7 | {**foo, **{"bar": 10}} # PIE800
|
= help: Remove unnecessary dict
Safe fix
2 2 |
3 3 | {**{"bar": 10}, "a": "b"} # PIE800
4 4 |
5 |-foo({**foo, **{"bar": True}}) # PIE800
5 |+foo({**foo, "bar": True}) # PIE800
6 6 |
7 7 | {**foo, **{"bar": 10}} # PIE800
8 8 |
PIE800.py:7:11: PIE800 [*] Unnecessary spread `**`
|
5 | foo({**foo, **{"bar": True}}) # PIE800
6 |
7 | {**foo, **{"bar": 10}} # PIE800
| ^^^^^^^^^^^ PIE800
8 |
9 | { # PIE800
|
= help: Remove unnecessary dict
Safe fix
4 4 |
5 5 | foo({**foo, **{"bar": True}}) # PIE800
6 6 |
7 |-{**foo, **{"bar": 10}} # PIE800
7 |+{**foo, "bar": 10} # PIE800
8 8 |
9 9 | { # PIE800
10 10 | "a": "b",
PIE800.py:12:7: PIE800 [*] Unnecessary spread `**`
|
10 | "a": "b",
11 | # Preserve
12 | **{
| _______^
13 | | # all
14 | | "bar": 10, # the
15 | | # comments
16 | | },
| |_____^ PIE800
17 | }
|
= help: Remove unnecessary dict
Safe fix
9 9 | { # PIE800
10 10 | "a": "b",
11 11 | # Preserve
12 |- **{
12 |+
13 13 | # all
14 |- "bar": 10, # the
14 |+ "bar": 10 # the
15 15 | # comments
16 |- },
16 |+ ,
17 17 | }
18 18 |
19 19 | {**foo, **buzz, **{bar: 10}} # PIE800
PIE800.py:19:19: PIE800 [*] Unnecessary spread `**`
|
17 | }
18 |
19 | {**foo, **buzz, **{bar: 10}} # PIE800
| ^^^^^^^^^ PIE800
20 |
21 | {**foo, "bar": True } # OK
|
= help: Remove unnecessary dict
Safe fix
16 16 | },
17 17 | }
18 18 |
19 |-{**foo, **buzz, **{bar: 10}} # PIE800
19 |+{**foo, **buzz, bar: 10} # PIE800
20 20 |
21 21 | {**foo, "bar": True } # OK
22 22 |

View File

@@ -97,11 +97,12 @@ pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) {
let mut diagnostic = Diagnostic::new(TrioZeroSleepCall, call.range());
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("trio", "lowlevel.checkpoint"),
&ImportRequest::import_from("trio", "lowlevel"),
call.func.start(),
checker.semantic(),
)?;
let reference_edit = Edit::range_replacement(binding, call.func.range());
let reference_edit =
Edit::range_replacement(format!("{binding}.checkpoint"), call.func.range());
let arg_edit = Edit::range_deletion(call.arguments.range);
Ok(Fix::safe_edits(import_edit, [reference_edit, arg_edit]))
});

View File

@@ -1,119 +1,107 @@
---
source: crates/ruff_linter/src/rules/flake8_trio/mod.rs
---
TRIO115.py:6:11: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
TRIO115.py:5:11: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
5 | async def func():
6 | await trio.sleep(0) # TRIO115
3 | from trio import sleep
4 |
5 | await trio.sleep(0) # TRIO115
| ^^^^^^^^^^^^^ TRIO115
7 | await trio.sleep(1) # OK
8 | await trio.sleep(0, 1) # OK
6 | await trio.sleep(1) # OK
7 | await trio.sleep(0, 1) # OK
|
= help: Replace with `trio.lowlevel.checkpoint()`
Safe fix
3 3 |
2 2 | import trio
3 3 | from trio import sleep
4 4 |
5 5 | async def func():
6 |- await trio.sleep(0) # TRIO115
6 |+ await trio.lowlevel.checkpoint # TRIO115
7 7 | await trio.sleep(1) # OK
8 8 | await trio.sleep(0, 1) # OK
9 9 | await trio.sleep(...) # OK
5 |- await trio.sleep(0) # 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
TRIO115.py:12:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
TRIO115.py:11:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
10 | await trio.sleep() # OK
11 |
12 | trio.sleep(0) # TRIO115
9 | await trio.sleep() # OK
10 |
11 | trio.sleep(0) # TRIO115
| ^^^^^^^^^^^^^ TRIO115
13 | foo = 0
14 | trio.sleep(foo) # TRIO115
12 | foo = 0
13 | trio.sleep(foo) # TRIO115
|
= help: Replace with `trio.lowlevel.checkpoint()`
Safe fix
9 9 | await trio.sleep(...) # OK
10 10 | await trio.sleep() # OK
11 11 |
12 |- trio.sleep(0) # TRIO115
12 |+ trio.lowlevel.checkpoint # TRIO115
13 13 | foo = 0
14 14 | trio.sleep(foo) # TRIO115
15 15 | trio.sleep(1) # OK
8 8 | await trio.sleep(...) # OK
9 9 | await trio.sleep() # OK
10 10 |
11 |- trio.sleep(0) # TRIO115
11 |+ trio.lowlevel.checkpoint # TRIO115
12 12 | foo = 0
13 13 | trio.sleep(foo) # TRIO115
14 14 | trio.sleep(1) # OK
TRIO115.py:14:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
TRIO115.py:13:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
12 | trio.sleep(0) # TRIO115
13 | foo = 0
14 | trio.sleep(foo) # TRIO115
11 | trio.sleep(0) # TRIO115
12 | foo = 0
13 | trio.sleep(foo) # TRIO115
| ^^^^^^^^^^^^^^^ TRIO115
15 | trio.sleep(1) # OK
16 | time.sleep(0) # OK
14 | trio.sleep(1) # OK
15 | time.sleep(0) # OK
|
= help: Replace with `trio.lowlevel.checkpoint()`
Safe fix
11 11 |
12 12 | trio.sleep(0) # TRIO115
13 13 | foo = 0
14 |- trio.sleep(foo) # TRIO115
14 |+ trio.lowlevel.checkpoint # TRIO115
15 15 | trio.sleep(1) # OK
16 16 | time.sleep(0) # OK
17 17 |
10 10 |
11 11 | trio.sleep(0) # TRIO115
12 12 | foo = 0
13 |- trio.sleep(foo) # TRIO115
13 |+ trio.lowlevel.checkpoint # TRIO115
14 14 | trio.sleep(1) # OK
15 15 | time.sleep(0) # OK
16 16 |
TRIO115.py:18:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
TRIO115.py:17:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
16 | time.sleep(0) # OK
17 |
18 | sleep(0) # TRIO115
15 | time.sleep(0) # OK
16 |
17 | sleep(0) # TRIO115
| ^^^^^^^^ TRIO115
19 |
20 | bar = "bar"
18 |
19 | bar = "bar"
|
= help: Replace with `trio.lowlevel.checkpoint()`
Safe fix
15 15 | trio.sleep(1) # OK
16 16 | time.sleep(0) # OK
17 17 |
18 |- sleep(0) # TRIO115
18 |+ trio.lowlevel.checkpoint # TRIO115
19 19 |
20 20 | bar = "bar"
21 21 | trio.sleep(bar)
14 14 | trio.sleep(1) # OK
15 15 | time.sleep(0) # OK
16 16 |
17 |- sleep(0) # TRIO115
17 |+ trio.lowlevel.checkpoint # TRIO115
18 18 |
19 19 | bar = "bar"
20 20 | trio.sleep(bar)
TRIO115.py:24:1: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
TRIO115.py:30:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
24 | trio.sleep(0) # TRIO115
| ^^^^^^^^^^^^^ TRIO115
29 | def func():
30 | sleep(0) # TRIO115
| ^^^^^^^^ TRIO115
|
= help: Replace with `trio.lowlevel.checkpoint()`
Safe fix
21 21 | trio.sleep(bar)
22 22 |
23 23 |
24 |-trio.sleep(0) # TRIO115
24 |+trio.lowlevel.checkpoint # TRIO115
24 24 | trio.run(trio.sleep(0)) # TRIO115
25 25 |
26 26 |
27 27 | def func():
TRIO115.py:28:14: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
27 | def func():
28 | trio.run(trio.sleep(0)) # TRIO115
| ^^^^^^^^^^^^^ TRIO115
|
= help: Replace with `trio.lowlevel.checkpoint()`
Safe fix
25 25 |
26 26 |
27 27 | def func():
28 |- trio.run(trio.sleep(0)) # TRIO115
28 |+ trio.run(trio.lowlevel.checkpoint) # TRIO115
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

View File

@@ -171,18 +171,18 @@ mod tests {
}
#[test_case(
r#"
r"
from __future__ import annotations
import pandas as pd
def f(x: pd.DataFrame):
pass
"#,
",
"no_typing_import"
)]
#[test_case(
r#"
r"
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -191,11 +191,11 @@ mod tests {
def f(x: pd.DataFrame):
pass
"#,
",
"typing_import_before_package_import"
)]
#[test_case(
r#"
r"
from __future__ import annotations
import pandas as pd
@@ -204,11 +204,11 @@ mod tests {
def f(x: pd.DataFrame):
pass
"#,
",
"typing_import_after_package_import"
)]
#[test_case(
r#"
r"
from __future__ import annotations
import pandas as pd
@@ -217,11 +217,11 @@ mod tests {
pass
from typing import TYPE_CHECKING
"#,
",
"typing_import_after_usage"
)]
#[test_case(
r#"
r"
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -233,11 +233,11 @@ mod tests {
def f(x: pd.DataFrame):
pass
"#,
",
"type_checking_block_own_line"
)]
#[test_case(
r#"
r"
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -248,11 +248,11 @@ mod tests {
def f(x: pd.DataFrame):
pass
"#,
",
"type_checking_block_inline"
)]
#[test_case(
r#"
r"
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -265,11 +265,11 @@ mod tests {
def f(x: pd.DataFrame):
pass
"#,
",
"type_checking_block_comment"
)]
#[test_case(
r#"
r"
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -281,11 +281,11 @@ mod tests {
if TYPE_CHECKING:
import os
"#,
",
"type_checking_block_after_usage"
)]
#[test_case(
r#"
r"
from __future__ import annotations
from pandas import (
@@ -295,11 +295,11 @@ mod tests {
def f(x: DataFrame):
pass
"#,
",
"import_from"
)]
#[test_case(
r#"
r"
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -314,11 +314,11 @@ mod tests {
def f(x: DataFrame):
pass
"#,
",
"import_from_type_checking_block"
)]
#[test_case(
r#"
r"
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -330,11 +330,11 @@ mod tests {
def f(x: DataFrame, y: Series):
pass
"#,
",
"multiple_members"
)]
#[test_case(
r#"
r"
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -343,11 +343,11 @@ mod tests {
def f(x: os, y: sys):
pass
"#,
",
"multiple_modules_same_type"
)]
#[test_case(
r#"
r"
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -356,7 +356,7 @@ mod tests {
def f(x: os, y: pandas):
pass
"#,
",
"multiple_modules_different_types"
)]
fn contents(contents: &str, snapshot: &str) {

View File

@@ -166,10 +166,10 @@ mod tests {
#[test]
fn trivial() -> Result<()> {
let source = r#"
let source = r"
def trivial():
pass
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 1);
Ok(())
@@ -177,10 +177,10 @@ def trivial():
#[test]
fn expr_as_statement() -> Result<()> {
let source = r#"
let source = r"
def expr_as_statement():
0xF00D
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 1);
Ok(())
@@ -188,12 +188,12 @@ def expr_as_statement():
#[test]
fn sequential() -> Result<()> {
let source = r#"
let source = r"
def sequential(n):
k = n + 4
s = k + n
return s
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 1);
Ok(())
@@ -234,11 +234,11 @@ def nested_ifs():
#[test]
fn for_loop() -> Result<()> {
let source = r#"
let source = r"
def for_loop():
for i in range(10):
print(i)
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 2);
Ok(())
@@ -246,13 +246,13 @@ def for_loop():
#[test]
fn for_else() -> Result<()> {
let source = r#"
let source = r"
def for_else(mylist):
for i in mylist:
print(i)
else:
print(None)
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 2);
Ok(())
@@ -260,13 +260,13 @@ def for_else(mylist):
#[test]
fn recursive() -> Result<()> {
let source = r#"
let source = r"
def recursive(n):
if n > 4:
return f(n - 1)
else:
return n
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 2);
Ok(())
@@ -274,7 +274,7 @@ def recursive(n):
#[test]
fn nested_functions() -> Result<()> {
let source = r#"
let source = r"
def nested_functions():
def a():
def b():
@@ -283,7 +283,7 @@ def nested_functions():
b()
a()
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 3);
Ok(())
@@ -291,7 +291,7 @@ def nested_functions():
#[test]
fn try_else() -> Result<()> {
let source = r#"
let source = r"
def try_else():
try:
print(1)
@@ -301,7 +301,7 @@ def try_else():
print(3)
else:
print(4)
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 4);
Ok(())
@@ -309,7 +309,7 @@ def try_else():
#[test]
fn nested_try_finally() -> Result<()> {
let source = r#"
let source = r"
def nested_try_finally():
try:
try:
@@ -318,7 +318,7 @@ def nested_try_finally():
print(2)
finally:
print(3)
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 1);
Ok(())
@@ -326,7 +326,7 @@ def nested_try_finally():
#[test]
fn foobar() -> Result<()> {
let source = r#"
let source = r"
async def foobar(a, b, c):
await whatever(a, b, c)
if await b:
@@ -335,7 +335,7 @@ async def foobar(a, b, c):
pass
async for x in a:
pass
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 3);
Ok(())
@@ -343,10 +343,10 @@ async def foobar(a, b, c):
#[test]
fn annotated_assign() -> Result<()> {
let source = r#"
let source = r"
def annotated_assign():
x: Any = None
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 1);
Ok(())
@@ -354,7 +354,7 @@ def annotated_assign():
#[test]
fn class() -> Result<()> {
let source = r#"
let source = r"
class Class:
def handle(self, *args, **options):
if args:
@@ -382,7 +382,7 @@ class Class:
pass
return ServiceProvider(Logger())
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 9);
Ok(())
@@ -390,13 +390,13 @@ class Class:
#[test]
fn finally() -> Result<()> {
let source = r#"
let source = r"
def process_detect_lines():
try:
pass
finally:
pass
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 1);
Ok(())
@@ -419,12 +419,12 @@ def process_detect_lines():
#[test]
fn with() -> Result<()> {
let source = r#"
let source = r"
def with_lock():
with lock:
if foo:
print('bar')
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(get_complexity_number(&stmts), 2);
Ok(())

View File

@@ -30,17 +30,17 @@ mod tests {
"PD002_fail"
)]
#[test_case(
r#"
r"
import pandas as pd
nas = pd.isna(val)
"#,
",
"PD003_pass"
)]
#[test_case(
r#"
r"
import pandas as pd
nulls = pd.isnull(val)
"#,
",
"PD003_fail"
)]
#[test_case(
@@ -51,17 +51,17 @@ mod tests {
"PD003_allows_other_calls"
)]
#[test_case(
r#"
r"
import pandas as pd
not_nas = pd.notna(val)
"#,
",
"PD004_pass"
)]
#[test_case(
r#"
r"
import pandas as pd
not_nulls = pd.notnull(val)
"#,
",
"PD004_fail"
)]
#[test_case(
@@ -73,11 +73,11 @@ mod tests {
"PD007_pass_loc"
)]
#[test_case(
r#"
r"
import pandas as pd
x = pd.DataFrame()
new_x = x.iloc[[1, 3, 5], [1, 3]]
"#,
",
"PD007_pass_iloc"
)]
#[test_case(
@@ -134,19 +134,19 @@ mod tests {
"PD008_fail"
)]
#[test_case(
r#"
r"
import pandas as pd
x = pd.DataFrame()
index = x.iloc[:, 1:3]
"#,
",
"PD009_pass"
)]
#[test_case(
r#"
r"
import pandas as pd
x = pd.DataFrame()
index = x.iat[:, 1:3]
"#,
",
"PD009_fail"
)]
#[test_case(
@@ -178,80 +178,80 @@ mod tests {
"PD010_fail_pivot"
)]
#[test_case(
r#"
r"
import pandas as pd
x = pd.DataFrame()
result = x.to_array()
"#,
",
"PD011_pass_to_array"
)]
#[test_case(
r#"
r"
import pandas as pd
x = pd.DataFrame()
result = x.array
"#,
",
"PD011_pass_array"
)]
#[test_case(
r#"
r"
import pandas as pd
x = pd.DataFrame()
result = x.values
"#,
",
"PD011_fail_values"
)]
#[test_case(
r#"
r"
import pandas as pd
x = pd.DataFrame()
result = x.values()
"#,
",
"PD011_pass_values_call"
)]
#[test_case(
r#"
r"
import pandas as pd
x = pd.DataFrame()
x.values = 1
"#,
",
"PD011_pass_values_store"
)]
#[test_case(
r#"
r"
class Class:
def __init__(self, values: str) -> None:
self.values = values
print(self.values)
"#,
",
"PD011_pass_values_instance"
)]
#[test_case(
r#"
r"
import pandas as pd
result = {}.values
"#,
",
"PD011_pass_values_dict"
)]
#[test_case(
r#"
r"
import pandas as pd
result = pd.values
"#,
",
"PD011_pass_values_import"
)]
#[test_case(
r#"
r"
import pandas as pd
result = x.values
"#,
",
"PD011_pass_values_unbound"
)]
#[test_case(
r#"
r"
import pandas as pd
result = values
"#,
",
"PD011_pass_node_name"
)]
#[test_case(
@@ -267,33 +267,33 @@ mod tests {
"PD013_pass"
)]
#[test_case(
r#"
r"
import numpy as np
arrays = [np.random.randn(3, 4) for _ in range(10)]
np.stack(arrays, axis=0).shape
"#,
",
"PD013_pass_numpy"
)]
#[test_case(
r#"
r"
import pandas as pd
y = x.stack(level=-1, dropna=True)
"#,
",
"PD013_pass_unbound"
)]
#[test_case(
r#"
r"
import pandas as pd
x = pd.DataFrame()
y = x.stack(level=-1, dropna=True)
"#,
",
"PD013_fail_stack"
)]
#[test_case(
r#"
r"
import pandas as pd
pd.stack(
"#,
",
"PD015_pass_merge_on_dataframe"
)]
#[test_case(
@@ -306,12 +306,12 @@ mod tests {
"PD015_pass_merge_on_dataframe_with_multiple_args"
)]
#[test_case(
r#"
r"
import pandas as pd
x = pd.DataFrame()
y = pd.DataFrame()
pd.merge(x, y)
"#,
",
"PD015_fail_merge_on_pandas_object"
)]
#[test_case(
@@ -321,31 +321,31 @@ mod tests {
"PD015_pass_other_pd_function"
)]
#[test_case(
r#"
r"
import pandas as pd
employees = pd.DataFrame(employee_dict)
"#,
",
"PD901_pass_non_df"
)]
#[test_case(
r#"
r"
import pandas as pd
employees_df = pd.DataFrame(employee_dict)
"#,
",
"PD901_pass_part_df"
)]
#[test_case(
r#"
r"
import pandas as pd
my_function(df=data)
"#,
",
"PD901_pass_df_param"
)]
#[test_case(
r#"
r"
import pandas as pd
df = pd.DataFrame()
"#,
",
"PD901_fail_df_var"
)]
fn contents(contents: &str, snapshot: &str) {

View File

@@ -24,13 +24,13 @@ use crate::settings::types::IdentifierPattern;
///
/// ## Example
/// ```python
/// def MY_FUNCTION():
/// def my_function(A, myArg):
/// pass
/// ```
///
/// Use instead:
/// ```python
/// def my_function():
/// def my_function(a, my_arg):
/// pass
/// ```
///

View File

@@ -518,10 +518,10 @@ mod tests {
#[test]
fn multi_line() {
assert_logical_lines(
r#"
r"
x = 1
y = 2
z = x + 1"#
z = x + 1"
.trim(),
&["x = 1", "y = 2", "z = x + 1"],
);
@@ -530,14 +530,14 @@ z = x + 1"#
#[test]
fn indented() {
assert_logical_lines(
r#"
r"
x = [
1,
2,
3,
]
y = 2
z = x + 1"#
z = x + 1"
.trim(),
&["x = [\n 1,\n 2,\n 3,\n]", "y = 2", "z = x + 1"],
);
@@ -551,11 +551,11 @@ z = x + 1"#
#[test]
fn function_definition() {
assert_logical_lines(
r#"
r"
def f():
x = 1
f()"#
.trim(),
f()"
.trim(),
&["def f():", "x = 1", "f()"],
);
}
@@ -583,11 +583,11 @@ f()"#
#[test]
fn empty_line() {
assert_logical_lines(
r#"
r"
if False:
print()
"#
"
.trim(),
&["if False:", "print()", ""],
);

View File

@@ -3,7 +3,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::docstrings::{clean_space, leading_space};
use ruff_source_file::NewlineWithTrailingNewline;
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextSize};
use ruff_text_size::{TextLen, TextRange};
use crate::checkers::ast::Checker;
@@ -172,6 +172,7 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
let mut has_seen_tab = docstring.indentation.contains('\t');
let mut is_over_indented = true;
let mut over_indented_lines = vec![];
let mut over_indented_offset = TextSize::from(u32::MAX);
for i in 0..lines.len() {
// First lines and continuations doesn't need any indentation.
@@ -217,7 +218,13 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
// the over-indentation status of every line.
if i < lines.len() - 1 {
if line_indent.len() > docstring.indentation.len() {
over_indented_lines.push(TextRange::at(line.start(), line_indent.text_len()));
over_indented_lines.push(line);
// Track the _smallest_ offset we see
over_indented_offset = std::cmp::min(
line_indent.text_len() - docstring.indentation.text_len(),
over_indented_offset,
);
} else {
is_over_indented = false;
}
@@ -235,16 +242,21 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
if checker.enabled(Rule::OverIndentation) {
// If every line (except the last) is over-indented...
if is_over_indented {
for over_indented in over_indented_lines {
for line in over_indented_lines {
let line_indent = leading_space(line);
let indent = clean_space(docstring.indentation);
// We report over-indentation on every line. This isn't great, but
// enables fix.
let mut diagnostic =
Diagnostic::new(OverIndentation, TextRange::empty(over_indented.start()));
let indent = clean_space(docstring.indentation);
Diagnostic::new(OverIndentation, TextRange::empty(line.start()));
let edit = if indent.is_empty() {
Edit::range_deletion(over_indented)
Edit::range_deletion(TextRange::at(line.start(), line_indent.text_len()))
} else {
Edit::range_replacement(indent, over_indented)
Edit::range_replacement(
indent.clone(),
TextRange::at(line.start(), indent.text_len() + over_indented_offset),
)
};
diagnostic.set_fix(Fix::safe_edit(edit));
checker.diagnostics.push(diagnostic);

View File

@@ -44,7 +44,7 @@ impl Violation for TripleSingleQuotes {
let TripleSingleQuotes { expected_quote } = self;
match expected_quote {
Quote::Double => format!(r#"Use triple double quotes `"""`"#),
Quote::Single => format!(r#"Use triple single quotes `'''`"#),
Quote::Single => format!(r"Use triple single quotes `'''`"),
}
}

View File

@@ -62,4 +62,353 @@ D.py:272:1: D208 [*] Docstring is over-indented
274 274 | """
275 275 |
D.py:673:1: D208 [*] Docstring is over-indented
|
671 | """Summary.
672 |
673 | This is overindented
| D208
674 | And so is this, but it we should preserve the extra space on this line relative
675 | to the one before
|
= help: Remove over-indentation
Safe fix
670 670 | def retain_extra_whitespace():
671 671 | """Summary.
672 672 |
673 |- This is overindented
673 |+ This is overindented
674 674 | And so is this, but it we should preserve the extra space on this line relative
675 675 | to the one before
676 676 | """
D.py:674:1: D208 [*] Docstring is over-indented
|
673 | This is overindented
674 | And so is this, but it we should preserve the extra space on this line relative
| D208
675 | to the one before
676 | """
|
= help: Remove over-indentation
Safe fix
671 671 | """Summary.
672 672 |
673 673 | This is overindented
674 |- And so is this, but it we should preserve the extra space on this line relative
674 |+ And so is this, but it we should preserve the extra space on this line relative
675 675 | to the one before
676 676 | """
677 677 |
D.py:675:1: D208 [*] Docstring is over-indented
|
673 | This is overindented
674 | And so is this, but it we should preserve the extra space on this line relative
675 | to the one before
| D208
676 | """
|
= help: Remove over-indentation
Safe fix
672 672 |
673 673 | This is overindented
674 674 | And so is this, but it we should preserve the extra space on this line relative
675 |- to the one before
675 |+ to the one before
676 676 | """
677 677 |
678 678 |
D.py:682:1: D208 [*] Docstring is over-indented
|
680 | """Summary.
681 |
682 | This is overindented
| D208
683 | And so is this, but it we should preserve the extra space on this line relative
684 | to the one before
|
= help: Remove over-indentation
Safe fix
679 679 | def retain_extra_whitespace_multiple():
680 680 | """Summary.
681 681 |
682 |- This is overindented
682 |+ This is overindented
683 683 | And so is this, but it we should preserve the extra space on this line relative
684 684 | to the one before
685 685 | This is also overindented
D.py:683:1: D208 [*] Docstring is over-indented
|
682 | This is overindented
683 | And so is this, but it we should preserve the extra space on this line relative
| D208
684 | to the one before
685 | This is also overindented
|
= help: Remove over-indentation
Safe fix
680 680 | """Summary.
681 681 |
682 682 | This is overindented
683 |- And so is this, but it we should preserve the extra space on this line relative
683 |+ And so is this, but it we should preserve the extra space on this line relative
684 684 | to the one before
685 685 | This is also overindented
686 686 | And so is this, but it we should preserve the extra space on this line relative
D.py:684:1: D208 [*] Docstring is over-indented
|
682 | This is overindented
683 | And so is this, but it we should preserve the extra space on this line relative
684 | to the one before
| D208
685 | This is also overindented
686 | And so is this, but it we should preserve the extra space on this line relative
|
= help: Remove over-indentation
Safe fix
681 681 |
682 682 | This is overindented
683 683 | And so is this, but it we should preserve the extra space on this line relative
684 |- to the one before
684 |+ to the one before
685 685 | This is also overindented
686 686 | And so is this, but it we should preserve the extra space on this line relative
687 687 | to the one before
D.py:685:1: D208 [*] Docstring is over-indented
|
683 | And so is this, but it we should preserve the extra space on this line relative
684 | to the one before
685 | This is also overindented
| D208
686 | And so is this, but it we should preserve the extra space on this line relative
687 | to the one before
|
= help: Remove over-indentation
Safe fix
682 682 | This is overindented
683 683 | And so is this, but it we should preserve the extra space on this line relative
684 684 | to the one before
685 |- This is also overindented
685 |+ This is also overindented
686 686 | And so is this, but it we should preserve the extra space on this line relative
687 687 | to the one before
688 688 | """
D.py:686:1: D208 [*] Docstring is over-indented
|
684 | to the one before
685 | This is also overindented
686 | And so is this, but it we should preserve the extra space on this line relative
| D208
687 | to the one before
688 | """
|
= help: Remove over-indentation
Safe fix
683 683 | And so is this, but it we should preserve the extra space on this line relative
684 684 | to the one before
685 685 | This is also overindented
686 |- And so is this, but it we should preserve the extra space on this line relative
686 |+ And so is this, but it we should preserve the extra space on this line relative
687 687 | to the one before
688 688 | """
689 689 |
D.py:687:1: D208 [*] Docstring is over-indented
|
685 | This is also overindented
686 | And so is this, but it we should preserve the extra space on this line relative
687 | to the one before
| D208
688 | """
|
= help: Remove over-indentation
Safe fix
684 684 | to the one before
685 685 | This is also overindented
686 686 | And so is this, but it we should preserve the extra space on this line relative
687 |- to the one before
687 |+ to the one before
688 688 | """
689 689 |
690 690 |
D.py:695:1: D208 [*] Docstring is over-indented
|
693 | """Summary.
694 |
695 | This is overindented
| D208
696 | And so is this, but it we should preserve the extra space on this line relative
697 | to the one before
|
= help: Remove over-indentation
Safe fix
692 692 | def retain_extra_whitespace_deeper():
693 693 | """Summary.
694 694 |
695 |- This is overindented
695 |+ This is overindented
696 696 | And so is this, but it we should preserve the extra space on this line relative
697 697 | to the one before
698 698 | And the relative indent here should be preserved too
D.py:696:1: D208 [*] Docstring is over-indented
|
695 | This is overindented
696 | And so is this, but it we should preserve the extra space on this line relative
| D208
697 | to the one before
698 | And the relative indent here should be preserved too
|
= help: Remove over-indentation
Safe fix
693 693 | """Summary.
694 694 |
695 695 | This is overindented
696 |- And so is this, but it we should preserve the extra space on this line relative
696 |+ And so is this, but it we should preserve the extra space on this line relative
697 697 | to the one before
698 698 | And the relative indent here should be preserved too
699 699 | """
D.py:697:1: D208 [*] Docstring is over-indented
|
695 | This is overindented
696 | And so is this, but it we should preserve the extra space on this line relative
697 | to the one before
| D208
698 | And the relative indent here should be preserved too
699 | """
|
= help: Remove over-indentation
Safe fix
694 694 |
695 695 | This is overindented
696 696 | And so is this, but it we should preserve the extra space on this line relative
697 |- to the one before
697 |+ to the one before
698 698 | And the relative indent here should be preserved too
699 699 | """
700 700 |
D.py:698:1: D208 [*] Docstring is over-indented
|
696 | And so is this, but it we should preserve the extra space on this line relative
697 | to the one before
698 | And the relative indent here should be preserved too
| D208
699 | """
|
= help: Remove over-indentation
Safe fix
695 695 | This is overindented
696 696 | And so is this, but it we should preserve the extra space on this line relative
697 697 | to the one before
698 |- And the relative indent here should be preserved too
698 |+ And the relative indent here should be preserved too
699 699 | """
700 700 |
701 701 | def retain_extra_whitespace_followed_by_same_offset():
D.py:704:1: D208 [*] Docstring is over-indented
|
702 | """Summary.
703 |
704 | This is overindented
| D208
705 | And so is this, but it we should preserve the extra space on this line relative
706 | This is overindented
|
= help: Remove over-indentation
Safe fix
701 701 | def retain_extra_whitespace_followed_by_same_offset():
702 702 | """Summary.
703 703 |
704 |- This is overindented
704 |+ This is overindented
705 705 | And so is this, but it we should preserve the extra space on this line relative
706 706 | This is overindented
707 707 | This is overindented
D.py:705:1: D208 [*] Docstring is over-indented
|
704 | This is overindented
705 | And so is this, but it we should preserve the extra space on this line relative
| D208
706 | This is overindented
707 | This is overindented
|
= help: Remove over-indentation
Safe fix
702 702 | """Summary.
703 703 |
704 704 | This is overindented
705 |- And so is this, but it we should preserve the extra space on this line relative
705 |+ And so is this, but it we should preserve the extra space on this line relative
706 706 | This is overindented
707 707 | This is overindented
708 708 | """
D.py:706:1: D208 [*] Docstring is over-indented
|
704 | This is overindented
705 | And so is this, but it we should preserve the extra space on this line relative
706 | This is overindented
| D208
707 | This is overindented
708 | """
|
= help: Remove over-indentation
Safe fix
703 703 |
704 704 | This is overindented
705 705 | And so is this, but it we should preserve the extra space on this line relative
706 |- This is overindented
706 |+ This is overindented
707 707 | This is overindented
708 708 | """
709 709 |
D.py:707:1: D208 [*] Docstring is over-indented
|
705 | And so is this, but it we should preserve the extra space on this line relative
706 | This is overindented
707 | This is overindented
| D208
708 | """
|
= help: Remove over-indentation
Safe fix
704 704 | This is overindented
705 705 | And so is this, but it we should preserve the extra space on this line relative
706 706 | This is overindented
707 |- This is overindented
707 |+ This is overindented
708 708 | """
709 709 |
710 710 |

View File

@@ -547,4 +547,136 @@ D.py:615:5: D213 [*] Multi-line docstring summary should start at the second lin
617 618 | """
618 619 |
D.py:671:5: D213 [*] Multi-line docstring summary should start at the second line
|
670 | def retain_extra_whitespace():
671 | """Summary.
| _____^
672 | |
673 | | This is overindented
674 | | And so is this, but it we should preserve the extra space on this line relative
675 | | to the one before
676 | | """
| |_______^ D213
|
= help: Insert line break and indentation after opening quotes
Safe fix
668 668 |
669 669 |
670 670 | def retain_extra_whitespace():
671 |- """Summary.
671 |+ """
672 |+ Summary.
672 673 |
673 674 | This is overindented
674 675 | And so is this, but it we should preserve the extra space on this line relative
D.py:680:5: D213 [*] Multi-line docstring summary should start at the second line
|
679 | def retain_extra_whitespace_multiple():
680 | """Summary.
| _____^
681 | |
682 | | This is overindented
683 | | And so is this, but it we should preserve the extra space on this line relative
684 | | to the one before
685 | | This is also overindented
686 | | And so is this, but it we should preserve the extra space on this line relative
687 | | to the one before
688 | | """
| |_______^ D213
|
= help: Insert line break and indentation after opening quotes
Safe fix
677 677 |
678 678 |
679 679 | def retain_extra_whitespace_multiple():
680 |- """Summary.
680 |+ """
681 |+ Summary.
681 682 |
682 683 | This is overindented
683 684 | And so is this, but it we should preserve the extra space on this line relative
D.py:693:5: D213 [*] Multi-line docstring summary should start at the second line
|
692 | def retain_extra_whitespace_deeper():
693 | """Summary.
| _____^
694 | |
695 | | This is overindented
696 | | And so is this, but it we should preserve the extra space on this line relative
697 | | to the one before
698 | | And the relative indent here should be preserved too
699 | | """
| |_______^ D213
700 |
701 | def retain_extra_whitespace_followed_by_same_offset():
|
= help: Insert line break and indentation after opening quotes
Safe fix
690 690 |
691 691 |
692 692 | def retain_extra_whitespace_deeper():
693 |- """Summary.
693 |+ """
694 |+ Summary.
694 695 |
695 696 | This is overindented
696 697 | And so is this, but it we should preserve the extra space on this line relative
D.py:702:5: D213 [*] Multi-line docstring summary should start at the second line
|
701 | def retain_extra_whitespace_followed_by_same_offset():
702 | """Summary.
| _____^
703 | |
704 | | This is overindented
705 | | And so is this, but it we should preserve the extra space on this line relative
706 | | This is overindented
707 | | This is overindented
708 | | """
| |_______^ D213
|
= help: Insert line break and indentation after opening quotes
Safe fix
699 699 | """
700 700 |
701 701 | def retain_extra_whitespace_followed_by_same_offset():
702 |- """Summary.
702 |+ """
703 |+ Summary.
703 704 |
704 705 | This is overindented
705 706 | And so is this, but it we should preserve the extra space on this line relative
D.py:712:5: D213 [*] Multi-line docstring summary should start at the second line
|
711 | def retain_extra_whitespace_not_overindented():
712 | """Summary.
| _____^
713 | |
714 | | This is not overindented
715 | | This is overindented, but since one line is not overindented this should not raise
716 | | And so is this, but it we should preserve the extra space on this line relative
717 | | """
| |_______^ D213
|
= help: Insert line break and indentation after opening quotes
Safe fix
709 709 |
710 710 |
711 711 | def retain_extra_whitespace_not_overindented():
712 |- """Summary.
712 |+ """
713 |+ Summary.
713 714 |
714 715 | This is not overindented
715 716 | This is overindented, but since one line is not overindented this should not raise

View File

@@ -326,5 +326,8 @@ D.py:664:5: D400 [*] First line should end with a period
664 664 | "We enforce a newline after the closing quote for a multi-line docstring \
665 |- but continuations shouldn't be considered multi-line"
665 |+ but continuations shouldn't be considered multi-line."
666 666 |
667 667 |
668 668 |

View File

@@ -308,5 +308,8 @@ D.py:664:5: D415 [*] First line should end with a period, question mark, or excl
664 664 | "We enforce a newline after the closing quote for a multi-line docstring \
665 |- but continuations shouldn't be considered multi-line"
665 |+ but continuations shouldn't be considered multi-line."
666 666 |
667 667 |
668 668 |

View File

@@ -196,5 +196,8 @@ D.py:664:5: D300 [*] Use triple double quotes `"""`
665 |- but continuations shouldn't be considered multi-line"
664 |+ """We enforce a newline after the closing quote for a multi-line docstring \
665 |+ but continuations shouldn't be considered multi-line"""
666 666 |
667 667 |
668 668 |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F821_21.py:4:1: F821 Undefined name `display`
|
3 | x = 1
4 | display(x)
| ^^^^^^^ F821
|

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -44,7 +44,7 @@ impl Violation for BadDunderMethodName {
#[derive_message_formats]
fn message(&self) -> String {
let BadDunderMethodName { name } = self;
format!("Bad or misspelled dunder method name `{name}`. (bad-dunder-name)")
format!("Bad or misspelled dunder method name `{name}`")
}
}

View File

@@ -187,7 +187,7 @@ mod tests {
#[test]
fn if_else_nested_if_else() -> Result<()> {
let source: &str = r#"
let source: &str = r"
if x == 0: # 3
return
else:
@@ -195,19 +195,19 @@ else:
pass
else:
pass
"#;
";
test_helper(source, 4)?;
Ok(())
}
#[test]
fn for_else() -> Result<()> {
let source: &str = r#"
let source: &str = r"
for _ in range(x): # 2
pass
else:
pass
"#;
";
test_helper(source, 2)?;
Ok(())
@@ -215,14 +215,14 @@ else:
#[test]
fn while_if_else_if() -> Result<()> {
let source: &str = r#"
let source: &str = r"
while x < 1: # 4
if x:
pass
else:
if x:
pass
"#;
";
test_helper(source, 4)?;
Ok(())
@@ -230,7 +230,7 @@ else:
#[test]
fn nested_def() -> Result<()> {
let source: &str = r#"
let source: &str = r"
if x: # 2
pass
else:
@@ -241,7 +241,7 @@ def g(x):
pass
return 1
"#;
";
test_helper(source, 2)?;
Ok(())
@@ -249,7 +249,7 @@ return 1
#[test]
fn try_except_except_else_finally() -> Result<()> {
let source: &str = r#"
let source: &str = r"
try:
pass
except:
@@ -260,7 +260,7 @@ else:
pass
finally:
pass
"#;
";
test_helper(source, 5)?;
Ok(())

View File

@@ -9,8 +9,8 @@ use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for classes with too many public methods
///
/// By default, this rule allows up to 20 statements, as configured by the
/// [`pylint.max-public-methods`] option.
/// By default, this rule allows up to 20 public methods, as configured by
/// the [`pylint.max-public-methods`] option.
///
/// ## Why is this bad?
/// Classes with many public methods are harder to understand

View File

@@ -110,7 +110,7 @@ mod tests {
#[test]
fn if_() -> Result<()> {
let source = r#"
let source = r"
x = 1
if x == 1: # 9
return
@@ -130,7 +130,7 @@ if x == 8:
return
if x == 9:
return
"#;
";
test_helper(source, 9)?;
Ok(())
@@ -138,12 +138,12 @@ if x == 9:
#[test]
fn for_else() -> Result<()> {
let source = r#"
let source = r"
for _i in range(10):
return
else:
return
"#;
";
test_helper(source, 2)?;
Ok(())
@@ -151,12 +151,12 @@ else:
#[test]
fn async_for_else() -> Result<()> {
let source = r#"
let source = r"
async for _i in range(10):
return
else:
return
"#;
";
test_helper(source, 2)?;
Ok(())
@@ -164,7 +164,7 @@ else:
#[test]
fn nested_def_ignored() -> Result<()> {
let source = r#"
let source = r"
def f():
return
@@ -173,7 +173,7 @@ if x == 1:
print()
else:
print()
"#;
";
test_helper(source, 0)?;
Ok(())
@@ -181,7 +181,7 @@ else:
#[test]
fn while_nested_if() -> Result<()> {
let source = r#"
let source = r"
x = 1
while x < 10:
print()
@@ -189,7 +189,7 @@ while x < 10:
return
x += 1
return
"#;
";
test_helper(source, 2)?;
Ok(())
@@ -197,12 +197,12 @@ return
#[test]
fn with_if() -> Result<()> {
let source = r#"
let source = r"
with a as f:
return
if f == 1:
return
"#;
";
test_helper(source, 2)?;
Ok(())
@@ -210,12 +210,12 @@ with a as f:
#[test]
fn async_with_if() -> Result<()> {
let source = r#"
let source = r"
async with a as f:
return
if f == 1:
return
"#;
";
test_helper(source, 2)?;
Ok(())
@@ -223,7 +223,7 @@ async with a as f:
#[test]
fn try_except_except_else_finally() -> Result<()> {
let source = r#"
let source = r"
try:
print()
return
@@ -235,7 +235,7 @@ else:
return
finally:
return
"#;
";
test_helper(source, 5)?;
Ok(())
@@ -243,14 +243,14 @@ finally:
#[test]
fn class_def_ignored() -> Result<()> {
let source = r#"
let source = r"
class A:
def f(self):
return
def g(self):
return
"#;
";
test_helper(source, 0)?;
Ok(())

View File

@@ -163,10 +163,10 @@ mod tests {
#[test]
fn pass() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
pass
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 2);
Ok(())
@@ -174,13 +174,13 @@ def f():
#[test]
fn if_else() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
if a:
print()
else:
print()
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 5);
Ok(())
@@ -188,14 +188,14 @@ def f():
#[test]
fn if_else_if_corner() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
if a:
print()
else:
if a:
print()
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 6);
Ok(())
@@ -203,13 +203,13 @@ def f():
#[test]
fn if_elif() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
if a:
print()
elif a:
print()
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 5);
Ok(())
@@ -217,7 +217,7 @@ def f():
#[test]
fn if_elif_else() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
if a:
print()
@@ -227,7 +227,7 @@ def f():
print()
else:
print()
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 9);
Ok(())
@@ -235,7 +235,7 @@ def f():
#[test]
fn many_statements() -> Result<()> {
let source: &str = r#"
let source: &str = r"
async def f():
a = 1
b = 2
@@ -256,7 +256,7 @@ async def f():
a -= 1
import time
pass
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 19);
Ok(())
@@ -264,11 +264,11 @@ async def f():
#[test]
fn for_() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
for i in range(10):
pass
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 2);
Ok(())
@@ -276,13 +276,13 @@ def f():
#[test]
fn for_else() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
for i in range(10):
print()
else:
print()
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 3);
Ok(())
@@ -290,14 +290,14 @@ def f():
#[test]
fn nested_def() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
def g():
print()
print()
print()
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 5);
Ok(())
@@ -305,7 +305,7 @@ def f():
#[test]
fn nested_class() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
class A:
def __init__(self):
@@ -315,7 +315,7 @@ def f():
pass
print()
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 3);
Ok(())
@@ -323,10 +323,10 @@ def f():
#[test]
fn return_not_counted() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
return
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 1);
Ok(())
@@ -334,7 +334,7 @@ def f():
#[test]
fn with() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
with a:
if a:
@@ -342,7 +342,7 @@ def f():
else:
print()
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 6);
Ok(())
@@ -350,13 +350,13 @@ def f():
#[test]
fn try_except() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
try:
print()
except Exception:
raise
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 5);
Ok(())
@@ -364,7 +364,7 @@ def f():
#[test]
fn try_except_else() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
try:
print()
@@ -372,7 +372,7 @@ def f():
pass
else:
print()
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 7);
Ok(())
@@ -380,7 +380,7 @@ def f():
#[test]
fn try_except_else_finally() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
try:
print()
@@ -390,7 +390,7 @@ def f():
print()
finally:
pass
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 10);
Ok(())
@@ -398,7 +398,7 @@ def f():
#[test]
fn try_except_except() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
try:
print()
@@ -406,7 +406,7 @@ def f():
pass
except Exception:
raise
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 8);
Ok(())
@@ -414,7 +414,7 @@ def f():
#[test]
fn try_except_except_finally() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
try:
print()
@@ -424,7 +424,7 @@ def f():
pass
finally:
print()
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 11);
Ok(())
@@ -432,11 +432,11 @@ def f():
#[test]
fn yield_() -> Result<()> {
let source: &str = r#"
let source: &str = r"
def f():
for i in range(10):
yield i
"#;
";
let stmts = parse_suite(source, "<filename>")?;
assert_eq!(num_statements(&stmts), 2);
Ok(())

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
bad_dunder_method_name.py:5:9: PLW3201 Bad or misspelled dunder method name `_init_`. (bad-dunder-name)
bad_dunder_method_name.py:5:9: PLW3201 Bad or misspelled dunder method name `_init_`
|
4 | class Apples:
5 | def _init_(self): # [bad-dunder-name]
@@ -9,7 +9,7 @@ bad_dunder_method_name.py:5:9: PLW3201 Bad or misspelled dunder method name `_in
6 | pass
|
bad_dunder_method_name.py:8:9: PLW3201 Bad or misspelled dunder method name `__hello__`. (bad-dunder-name)
bad_dunder_method_name.py:8:9: PLW3201 Bad or misspelled dunder method name `__hello__`
|
6 | pass
7 |
@@ -18,7 +18,7 @@ bad_dunder_method_name.py:8:9: PLW3201 Bad or misspelled dunder method name `__h
9 | print("hello")
|
bad_dunder_method_name.py:11:9: PLW3201 Bad or misspelled dunder method name `__init_`. (bad-dunder-name)
bad_dunder_method_name.py:11:9: PLW3201 Bad or misspelled dunder method name `__init_`
|
9 | print("hello")
10 |
@@ -28,7 +28,7 @@ bad_dunder_method_name.py:11:9: PLW3201 Bad or misspelled dunder method name `__
13 | pass
|
bad_dunder_method_name.py:15:9: PLW3201 Bad or misspelled dunder method name `_init_`. (bad-dunder-name)
bad_dunder_method_name.py:15:9: PLW3201 Bad or misspelled dunder method name `_init_`
|
13 | pass
14 |
@@ -38,7 +38,7 @@ bad_dunder_method_name.py:15:9: PLW3201 Bad or misspelled dunder method name `_i
17 | pass
|
bad_dunder_method_name.py:19:9: PLW3201 Bad or misspelled dunder method name `___neg__`. (bad-dunder-name)
bad_dunder_method_name.py:19:9: PLW3201 Bad or misspelled dunder method name `___neg__`
|
17 | pass
18 |
@@ -48,7 +48,7 @@ bad_dunder_method_name.py:19:9: PLW3201 Bad or misspelled dunder method name `__
21 | pass
|
bad_dunder_method_name.py:23:9: PLW3201 Bad or misspelled dunder method name `__inv__`. (bad-dunder-name)
bad_dunder_method_name.py:23:9: PLW3201 Bad or misspelled dunder method name `__inv__`
|
21 | pass
22 |

View File

@@ -104,145 +104,145 @@ mod tests {
#[test]
fn once() {
let source = r#"from foo import bar, baz, bop, qux as q"#;
let expected = r#"from foo import bar, baz, qux as q"#;
let source = r"from foo import bar, baz, bop, qux as q";
let expected = r"from foo import bar, baz, qux as q";
let actual = remove_import_members(source, &["bop"]);
assert_eq!(expected, actual);
}
#[test]
fn twice() {
let source = r#"from foo import bar, baz, bop, qux as q"#;
let expected = r#"from foo import bar, qux as q"#;
let source = r"from foo import bar, baz, bop, qux as q";
let expected = r"from foo import bar, qux as q";
let actual = remove_import_members(source, &["baz", "bop"]);
assert_eq!(expected, actual);
}
#[test]
fn aliased() {
let source = r#"from foo import bar, baz, bop as boop, qux as q"#;
let expected = r#"from foo import bar, baz, qux as q"#;
let source = r"from foo import bar, baz, bop as boop, qux as q";
let expected = r"from foo import bar, baz, qux as q";
let actual = remove_import_members(source, &["bop"]);
assert_eq!(expected, actual);
}
#[test]
fn parenthesized() {
let source = r#"from foo import (bar, baz, bop, qux as q)"#;
let expected = r#"from foo import (bar, baz, qux as q)"#;
let source = r"from foo import (bar, baz, bop, qux as q)";
let expected = r"from foo import (bar, baz, qux as q)";
let actual = remove_import_members(source, &["bop"]);
assert_eq!(expected, actual);
}
#[test]
fn last_import() {
let source = r#"from foo import bar, baz, bop, qux as q"#;
let expected = r#"from foo import bar, baz, bop"#;
let source = r"from foo import bar, baz, bop, qux as q";
let expected = r"from foo import bar, baz, bop";
let actual = remove_import_members(source, &["qux"]);
assert_eq!(expected, actual);
}
#[test]
fn first_import() {
let source = r#"from foo import bar, baz, bop, qux as q"#;
let expected = r#"from foo import baz, bop, qux as q"#;
let source = r"from foo import bar, baz, bop, qux as q";
let expected = r"from foo import baz, bop, qux as q";
let actual = remove_import_members(source, &["bar"]);
assert_eq!(expected, actual);
}
#[test]
fn first_two_imports() {
let source = r#"from foo import bar, baz, bop, qux as q"#;
let expected = r#"from foo import bop, qux as q"#;
let source = r"from foo import bar, baz, bop, qux as q";
let expected = r"from foo import bop, qux as q";
let actual = remove_import_members(source, &["bar", "baz"]);
assert_eq!(expected, actual);
}
#[test]
fn first_two_imports_multiline() {
let source = r#"from foo import (
let source = r"from foo import (
bar,
baz,
bop,
qux as q
)"#;
let expected = r#"from foo import (
)";
let expected = r"from foo import (
bop,
qux as q
)"#;
)";
let actual = remove_import_members(source, &["bar", "baz"]);
assert_eq!(expected, actual);
}
#[test]
fn multiline_once() {
let source = r#"from foo import (
let source = r"from foo import (
bar,
baz,
bop,
qux as q,
)"#;
let expected = r#"from foo import (
)";
let expected = r"from foo import (
bar,
baz,
qux as q,
)"#;
)";
let actual = remove_import_members(source, &["bop"]);
assert_eq!(expected, actual);
}
#[test]
fn multiline_twice() {
let source = r#"from foo import (
let source = r"from foo import (
bar,
baz,
bop,
qux as q,
)"#;
let expected = r#"from foo import (
)";
let expected = r"from foo import (
bar,
qux as q,
)"#;
)";
let actual = remove_import_members(source, &["baz", "bop"]);
assert_eq!(expected, actual);
}
#[test]
fn multiline_comment() {
let source = r#"from foo import (
let source = r"from foo import (
bar,
baz,
# This comment should be removed.
bop,
# This comment should be retained.
qux as q,
)"#;
let expected = r#"from foo import (
)";
let expected = r"from foo import (
bar,
baz,
# This comment should be retained.
qux as q,
)"#;
)";
let actual = remove_import_members(source, &["bop"]);
assert_eq!(expected, actual);
}
#[test]
fn multi_comment_first_import() {
let source = r#"from foo import (
let source = r"from foo import (
# This comment should be retained.
bar,
# This comment should be removed.
baz,
bop,
qux as q,
)"#;
let expected = r#"from foo import (
)";
let expected = r"from foo import (
# This comment should be retained.
baz,
bop,
qux as q,
)"#;
)";
let actual = remove_import_members(source, &["bar"]);
assert_eq!(expected, actual);
}

View File

@@ -19,3 +19,16 @@ pub(super) fn curly_escape(text: &str) -> Cow<'_, str> {
}
})
}
static DOUBLE_CURLY_BRACES: Lazy<Regex> = Lazy::new(|| Regex::new(r"((\{\{)|(\}\}))").unwrap());
pub(super) fn curly_unescape(text: &str) -> Cow<'_, str> {
// Match all double curly braces and replace with a single
DOUBLE_CURLY_BRACES.replace_all(text, |caps: &Captures| {
if &caps[1] == "{{" {
"{".to_string()
} else {
"}".to_string()
}
})
}

View File

@@ -18,7 +18,7 @@ use crate::checkers::ast::Checker;
use crate::fix::edits::fits_or_shrinks;
use crate::rules::pyflakes::format::FormatSummary;
use crate::rules::pyupgrade::helpers::curly_escape;
use crate::rules::pyupgrade::helpers::{curly_escape, curly_unescape};
/// ## What it does
/// Checks for `str.format` calls that can be replaced with f-strings.
@@ -357,9 +357,11 @@ pub(crate) fn f_strings(
Some((Tok::String { .. }, range)) => {
match try_convert_to_f_string(range, &mut summary, checker.locator()) {
Ok(Some(fstring)) => patches.push((range, fstring)),
// Skip any strings that don't require conversion (e.g., literal segments of an
// implicit concatenation).
Ok(None) => continue,
// Convert escaped curly brackets e.g. `{{` to `{` in literal string parts
Ok(None) => patches.push((
range,
curly_unescape(checker.locator().slice(range)).to_string(),
)),
// If any of the segments fail to convert, then we can't convert the entire
// expression.
Err(_) => return,
@@ -384,7 +386,21 @@ pub(crate) fn f_strings(
contents.push_str(&fstring);
prev_end = range.end();
}
contents.push_str(checker.locator().slice(TextRange::new(prev_end, end)));
// If the remainder is non-empty, add it to the contents.
let rest = checker.locator().slice(TextRange::new(prev_end, end));
if !lexer::lex_starts_at(rest, Mode::Expression, prev_end)
.flatten()
.all(|(token, _)| match token {
Tok::Comment(_) | Tok::Newline | Tok::NonLogicalNewline | Tok::Indent | Tok::Dedent => {
true
}
Tok::String { value, .. } => value.is_empty(),
_ => false,
})
{
contents.push_str(rest);
}
// If necessary, add a space between any leading keyword (`return`, `yield`, `assert`, etc.)
// and the string. For example, `return"foo"` is valid, but `returnf"foo"` is not.

View File

@@ -43,7 +43,7 @@ impl Violation for ReplaceStdoutStderr {
#[derive_message_formats]
fn message(&self) -> String {
format!("Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`")
format!("Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`")
}
fn fix_title(&self) -> Option<String> {

View File

@@ -1,9 +1,10 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::ExprStringLiteral;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{StringKind, Tok};
use ruff_text_size::{Ranged, TextRange, TextSize};
/// ## What it does
/// Checks for uses of the Unicode kind prefix (`u`) in strings.
@@ -39,13 +40,19 @@ impl AlwaysFixableViolation for UnicodeKindPrefix {
}
/// UP025
pub(crate) fn unicode_kind_prefix(checker: &mut Checker, string: &ExprStringLiteral) {
if string.unicode {
let mut diagnostic = Diagnostic::new(UnicodeKindPrefix, string.range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(TextRange::at(
string.start(),
TextSize::from(1),
))));
checker.diagnostics.push(diagnostic);
pub(crate) fn unicode_kind_prefix(diagnostics: &mut Vec<Diagnostic>, tokens: &[LexResult]) {
for (token, range) in tokens.iter().flatten() {
if let Tok::String {
kind: StringKind::Unicode,
..
} = token
{
let mut diagnostic = Diagnostic::new(UnicodeKindPrefix, *range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(TextRange::at(
range.start(),
TextSize::from(1),
))));
diagnostics.push(diagnostic);
}
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP022.py:4:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`
UP022.py:4:10: UP022 [*] Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`
|
2 | import subprocess
3 |
@@ -22,7 +22,7 @@ UP022.py:4:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated,
6 6 | output = subprocess.run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
7 7 |
UP022.py:6:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`
UP022.py:6:10: UP022 [*] Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`
|
4 | output = run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
5 |
@@ -43,7 +43,7 @@ UP022.py:6:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated,
8 8 | output = subprocess.run(stdout=subprocess.PIPE, args=["foo"], stderr=subprocess.PIPE)
9 9 |
UP022.py:8:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`
UP022.py:8:10: UP022 [*] Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`
|
6 | output = subprocess.run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
7 |
@@ -64,7 +64,7 @@ UP022.py:8:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated,
10 10 | output = subprocess.run(
11 11 | ["foo"], stdout=subprocess.PIPE, check=True, stderr=subprocess.PIPE
UP022.py:10:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`
UP022.py:10:10: UP022 [*] Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`
|
8 | output = subprocess.run(stdout=subprocess.PIPE, args=["foo"], stderr=subprocess.PIPE)
9 |
@@ -88,7 +88,7 @@ UP022.py:10:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated,
13 13 |
14 14 | output = subprocess.run(
UP022.py:14:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`
UP022.py:14:10: UP022 [*] Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`
|
12 | )
13 |
@@ -112,7 +112,7 @@ UP022.py:14:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated,
17 17 |
18 18 | output = subprocess.run(
UP022.py:18:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`
UP022.py:18:10: UP022 [*] Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`
|
16 | )
17 |
@@ -144,7 +144,7 @@ UP022.py:18:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated,
24 23 | encoding="utf-8",
25 24 | close_fds=True,
UP022.py:29:14: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`
UP022.py:29:14: UP022 [*] Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`
|
28 | if output:
29 | output = subprocess.run(
@@ -174,7 +174,7 @@ UP022.py:29:14: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated,
35 34 | encoding="utf-8",
36 35 | )
UP022.py:38:10: UP022 Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`
UP022.py:38:10: UP022 Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`
|
36 | )
37 |
@@ -188,7 +188,7 @@ UP022.py:38:10: UP022 Sending `stdout` and `stderr` to `PIPE` is deprecated, use
|
= help: Replace with `capture_output` keyword argument
UP022.py:42:10: UP022 Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`
UP022.py:42:10: UP022 Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`
|
40 | )
41 |
@@ -202,7 +202,7 @@ UP022.py:42:10: UP022 Sending `stdout` and `stderr` to `PIPE` is deprecated, use
|
= help: Replace with `capture_output` keyword argument
UP022.py:46:10: UP022 Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`
UP022.py:46:10: UP022 Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE`
|
44 | )
45 |

View File

@@ -248,4 +248,37 @@ UP025.py:19:5: UP025 [*] Remove unicode literals from strings
21 21 | # These should not change
22 22 | u = "Hello"
UP025.py:29:7: UP025 [*] Remove unicode literals from strings
|
27 | return"Hello"
28 |
29 | f"foo"u"bar"
| ^^^^^^ UP025
30 | f"foo" u"bar"
|
= help: Remove unicode prefix
Safe fix
26 26 | def hello():
27 27 | return"Hello"
28 28 |
29 |-f"foo"u"bar"
29 |+f"foo""bar"
30 30 | f"foo" u"bar"
UP025.py:30:8: UP025 [*] Remove unicode literals from strings
|
29 | f"foo"u"bar"
30 | f"foo" u"bar"
| ^^^^^^ UP025
|
= help: Remove unicode prefix
Safe fix
27 27 | return"Hello"
28 28 |
29 29 | f"foo"u"bar"
30 |-f"foo" u"bar"
30 |+f"foo" "bar"

View File

@@ -962,6 +962,8 @@ UP032_0.py:209:1: UP032 [*] Use f-string instead of `format` call
208 | # existing line length, so it's fine.
209 | "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
210 |
211 | # When fixing, trim the trailing empty string.
|
= help: Convert to f-string
@@ -971,5 +973,189 @@ UP032_0.py:209:1: UP032 [*] Use f-string instead of `format` call
208 208 | # existing line length, so it's fine.
209 |-"<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
209 |+f"<Customer: {self.internal_ids}, {self.external_ids}, {self.properties}, {self.tags}, {self.others}>"
210 210 |
211 211 | # When fixing, trim the trailing empty string.
212 212 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
UP032_0.py:212:18: UP032 [*] Use f-string instead of `format` call
|
211 | # When fixing, trim the trailing empty string.
212 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
| __________________^
213 | | "".format(new_dict, d))
| |_______________________________________^ UP032
214 |
215 | # When fixing, trim the trailing empty string.
|
= help: Convert to f-string
Safe fix
209 209 | "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
210 210 |
211 211 | # When fixing, trim the trailing empty string.
212 |-raise ValueError("Conflicting configuration dicts: {!r} {!r}"
213 |- "".format(new_dict, d))
212 |+raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}"
213 |+ "")
214 214 |
215 215 | # When fixing, trim the trailing empty string.
216 216 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
UP032_0.py:216:18: UP032 [*] Use f-string instead of `format` call
|
215 | # When fixing, trim the trailing empty string.
216 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
| __________________^
217 | | .format(new_dict, d))
| |_____________________________________^ UP032
218 |
219 | raise ValueError(
|
= help: Convert to f-string
Safe fix
213 213 | "".format(new_dict, d))
214 214 |
215 215 | # When fixing, trim the trailing empty string.
216 |-raise ValueError("Conflicting configuration dicts: {!r} {!r}"
217 |- .format(new_dict, d))
216 |+raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}")
218 217 |
219 218 | raise ValueError(
220 219 | "Conflicting configuration dicts: {!r} {!r}"
UP032_0.py:220:5: UP032 [*] Use f-string instead of `format` call
|
219 | raise ValueError(
220 | "Conflicting configuration dicts: {!r} {!r}"
| _____^
221 | | "".format(new_dict, d)
| |__________________________^ UP032
222 | )
|
= help: Convert to f-string
Safe fix
217 217 | .format(new_dict, d))
218 218 |
219 219 | raise ValueError(
220 |- "Conflicting configuration dicts: {!r} {!r}"
221 |- "".format(new_dict, d)
220 |+ f"Conflicting configuration dicts: {new_dict!r} {d!r}"
221 |+ ""
222 222 | )
223 223 |
224 224 | raise ValueError(
UP032_0.py:225:5: UP032 [*] Use f-string instead of `format` call
|
224 | raise ValueError(
225 | "Conflicting configuration dicts: {!r} {!r}"
| _____^
226 | | "".format(new_dict, d)
| |__________________________^ UP032
227 |
228 | )
|
= help: Convert to f-string
Safe fix
222 222 | )
223 223 |
224 224 | raise ValueError(
225 |- "Conflicting configuration dicts: {!r} {!r}"
226 |- "".format(new_dict, d)
225 |+ f"Conflicting configuration dicts: {new_dict!r} {d!r}"
226 |+ ""
227 227 |
228 228 | )
229 229 |
UP032_0.py:231:1: UP032 [*] Use f-string instead of `format` call
|
230 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped
231 | / (
232 | | "{}"
233 | | "{{}}"
234 | | ).format(a)
| |___________^ UP032
235 |
236 | ("{}" "{{}}").format(a)
|
= help: Convert to f-string
Safe fix
229 229 |
230 230 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped
231 231 | (
232 |+ f"{a}"
232 233 | "{}"
233 |- "{{}}"
234 |-).format(a)
234 |+)
235 235 |
236 236 | ("{}" "{{}}").format(a)
237 237 |
UP032_0.py:236:1: UP032 [*] Use f-string instead of `format` call
|
234 | ).format(a)
235 |
236 | ("{}" "{{}}").format(a)
| ^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
= help: Convert to f-string
Safe fix
233 233 | "{{}}"
234 234 | ).format(a)
235 235 |
236 |-("{}" "{{}}").format(a)
236 |+(f"{a}" "{}")
237 237 |
238 238 |
239 239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
UP032_0.py:240:1: UP032 [*] Use f-string instead of `format` call
|
239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
240 | / (
241 | | "{}"
242 | | "{{{}}}"
243 | | ).format(a, b)
| |______________^ UP032
244 |
245 | ("{}" "{{{}}}").format(a, b)
|
= help: Convert to f-string
Safe fix
238 238 |
239 239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
240 240 | (
241 |- "{}"
242 |- "{{{}}}"
243 |-).format(a, b)
241 |+ f"{a}"
242 |+ f"{{{b}}}"
243 |+)
244 244 |
245 245 | ("{}" "{{{}}}").format(a, b)
UP032_0.py:245:1: UP032 [*] Use f-string instead of `format` call
|
243 | ).format(a, b)
244 |
245 | ("{}" "{{{}}}").format(a, b)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
= help: Convert to f-string
Safe fix
242 242 | "{{{}}}"
243 243 | ).format(a, b)
244 244 |
245 |-("{}" "{{{}}}").format(a, b)
245 |+(f"{a}" f"{{{b}}}")

View File

@@ -18,9 +18,11 @@ mod tests {
#[test_case(Rule::RepeatedAppend, Path::new("FURB113.py"))]
#[test_case(Rule::DeleteFullSlice, Path::new("FURB131.py"))]
#[test_case(Rule::CheckAndRemoveFromSet, Path::new("FURB132.py"))]
#[test_case(Rule::IfExprMinMax, Path::new("FURB136.py"))]
#[test_case(Rule::ReimplementedStarmap, Path::new("FURB140.py"))]
#[test_case(Rule::SliceCopy, Path::new("FURB145.py"))]
#[test_case(Rule::UnnecessaryEnumerate, Path::new("FURB148.py"))]
#[test_case(Rule::MathConstant, Path::new("FURB152.py"))]
#[test_case(Rule::PrintEmptyString, Path::new("FURB105.py"))]
#[test_case(Rule::ImplicitCwd, Path::new("FURB177.py"))]
#[test_case(Rule::SingleItemMembershipTest, Path::new("FURB171.py"))]

View File

@@ -0,0 +1,178 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::{self as ast, CmpOp, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::snippet::SourceCodeSnippet;
/// ## What it does
/// Checks for `if` expressions that can be replaced with `min()` or `max()`
/// calls.
///
/// ## Why is this bad?
/// An `if` expression that selects the lesser or greater of two
/// sub-expressions can be replaced with a `min()` or `max()` call
/// respectively. When possible, prefer `min()` and `max()`, as they're more
/// concise and readable than the equivalent `if` expression.
///
/// ## Example
/// ```python
/// highest_score = score1 if score1 > score2 else score2
/// ```
///
/// Use instead:
/// ```python
/// highest_score = max(score2, score1)
/// ```
///
/// ## References
/// - [Python documentation: `min`](https://docs.python.org/3.11/library/functions.html#min)
/// - [Python documentation: `max`](https://docs.python.org/3.11/library/functions.html#max)
#[violation]
pub struct IfExprMinMax {
min_max: MinMax,
expression: SourceCodeSnippet,
replacement: SourceCodeSnippet,
}
impl Violation for IfExprMinMax {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let Self {
min_max,
expression,
replacement,
} = self;
match (expression.full_display(), replacement.full_display()) {
(_, None) => {
format!("Replace `if` expression with `{min_max}` call")
}
(None, Some(replacement)) => {
format!("Replace `if` expression with `{replacement}`")
}
(Some(expression), Some(replacement)) => {
format!("Replace `{expression}` with `{replacement}`")
}
}
}
fn fix_title(&self) -> Option<String> {
let Self {
replacement,
min_max,
..
} = self;
if let Some(replacement) = replacement.full_display() {
Some(format!("Replace with `{replacement}`"))
} else {
Some(format!("Replace with `{min_max}` call"))
}
}
}
/// FURB136
pub(crate) fn if_expr_min_max(checker: &mut Checker, if_exp: &ast::ExprIfExp) {
let Expr::Compare(ast::ExprCompare {
left,
ops,
comparators,
..
}) = if_exp.test.as_ref()
else {
return;
};
// Ignore, e.g., `foo < bar < baz`.
let [op] = ops.as_slice() else {
return;
};
// Determine whether to use `min()` or `max()`, and whether to flip the
// order of the arguments, which is relevant for breaking ties.
let (mut min_max, mut flip_args) = match op {
CmpOp::Gt => (MinMax::Max, true),
CmpOp::GtE => (MinMax::Max, false),
CmpOp::Lt => (MinMax::Min, true),
CmpOp::LtE => (MinMax::Min, false),
_ => return,
};
let [right] = comparators.as_slice() else {
return;
};
let body_cmp = ComparableExpr::from(if_exp.body.as_ref());
let orelse_cmp = ComparableExpr::from(if_exp.orelse.as_ref());
let left_cmp = ComparableExpr::from(left);
let right_cmp = ComparableExpr::from(right);
if body_cmp == right_cmp && orelse_cmp == left_cmp {
min_max = min_max.reverse();
flip_args = !flip_args;
} else if body_cmp != left_cmp || orelse_cmp != right_cmp {
return;
}
let (arg1, arg2) = if flip_args {
(right, left.as_ref())
} else {
(left.as_ref(), right)
};
let replacement = format!(
"{min_max}({}, {})",
checker.generator().expr(arg1),
checker.generator().expr(arg2),
);
let mut diagnostic = Diagnostic::new(
IfExprMinMax {
min_max,
expression: SourceCodeSnippet::from_str(checker.locator().slice(if_exp)),
replacement: SourceCodeSnippet::from_str(replacement.as_str()),
},
if_exp.range(),
);
if checker.semantic().is_builtin(min_max.as_str()) {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
replacement,
if_exp.range(),
)));
}
checker.diagnostics.push(diagnostic);
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum MinMax {
Min,
Max,
}
impl MinMax {
fn reverse(self) -> Self {
match self {
Self::Min => Self::Max,
Self::Max => Self::Min,
}
}
fn as_str(self) -> &'static str {
match self {
Self::Min => "min",
Self::Max => "max",
}
}
}
impl std::fmt::Display for MinMax {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "{}", self.as_str())
}
}

View File

@@ -0,0 +1,90 @@
use anyhow::Result;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Number};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
/// ## What it does
/// Checks for literals that are similar to constants in `math` module.
///
/// ## Why is this bad?
/// Hard-coding mathematical constants like π increases code duplication,
/// reduces readability, and may lead to a lack of precision.
///
/// ## Example
/// ```python
/// A = 3.141592 * r**2
/// ```
///
/// Use instead:
/// ```python
/// A = math.pi * r**2
/// ```
///
/// ## References
/// - [Python documentation: `math` constants](https://docs.python.org/3/library/math.html#constants)
#[violation]
pub struct MathConstant {
literal: String,
constant: &'static str,
}
impl Violation for MathConstant {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let MathConstant { literal, constant } = self;
format!("Replace `{literal}` with `math.{constant}`")
}
fn fix_title(&self) -> Option<String> {
let MathConstant { constant, .. } = self;
Some(format!("Use `math.{constant}`"))
}
}
/// FURB152
pub(crate) fn math_constant(checker: &mut Checker, literal: &ast::ExprNumberLiteral) {
let Number::Float(value) = literal.value else {
return;
};
for (real_value, constant) in [
(std::f64::consts::PI, "pi"),
(std::f64::consts::E, "e"),
(std::f64::consts::TAU, "tau"),
] {
if (value - real_value).abs() < 1e-2 {
let mut diagnostic = Diagnostic::new(
MathConstant {
literal: checker.locator().slice(literal).into(),
constant,
},
literal.range(),
);
diagnostic.try_set_fix(|| convert_to_constant(literal, constant, checker));
checker.diagnostics.push(diagnostic);
return;
}
}
}
fn convert_to_constant(
literal: &ast::ExprNumberLiteral,
constant: &'static str,
checker: &Checker,
) -> Result<Fix> {
let (edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("math", constant),
literal.start(),
checker.semantic(),
)?;
Ok(Fix::safe_edits(
Edit::range_replacement(binding, literal.range()),
[edit],
))
}

View File

@@ -1,7 +1,9 @@
pub(crate) use check_and_remove_from_set::*;
pub(crate) use delete_full_slice::*;
pub(crate) use if_expr_min_max::*;
pub(crate) use implicit_cwd::*;
pub(crate) use isinstance_type_none::*;
pub(crate) use math_constant::*;
pub(crate) use print_empty_string::*;
pub(crate) use read_whole_file::*;
pub(crate) use reimplemented_starmap::*;
@@ -13,8 +15,10 @@ pub(crate) use unnecessary_enumerate::*;
mod check_and_remove_from_set;
mod delete_full_slice;
mod if_expr_min_max;
mod implicit_cwd;
mod isinstance_type_none;
mod math_constant;
mod print_empty_string;
mod read_whole_file;
mod reimplemented_starmap;

View File

@@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_codegen::Generator;
use ruff_python_semantic::analyze::typing::is_list;
use ruff_python_semantic::{Binding, BindingId, DefinitionId, SemanticModel};
use ruff_python_semantic::{Binding, BindingId, SemanticModel};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@@ -183,8 +183,7 @@ fn match_consecutive_appends<'a>(
let siblings: &[Stmt] = if semantic.at_top_level() {
// If the statement is at the top level, we should go to the parent module.
// Module is available in the definitions list.
let module = semantic.definitions[DefinitionId::module()].as_module()?;
module.python_ast
semantic.definitions.python_ast()?
} else {
// Otherwise, go to the parent, and take its body as a sequence of siblings.
semantic

View File

@@ -0,0 +1,194 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB136.py:4:1: FURB136 [*] Replace `x if x > y else y` with `max(y, x)`
|
2 | y = 2
3 |
4 | x if x > y else y # FURB136
| ^^^^^^^^^^^^^^^^^ FURB136
5 |
6 | x if x >= y else y # FURB136
|
= help: Replace with `max(y, x)`
Safe fix
1 1 | x = 1
2 2 | y = 2
3 3 |
4 |-x if x > y else y # FURB136
4 |+max(y, x) # FURB136
5 5 |
6 6 | x if x >= y else y # FURB136
7 7 |
FURB136.py:6:1: FURB136 [*] Replace `x if x >= y else y` with `max(x, y)`
|
4 | x if x > y else y # FURB136
5 |
6 | x if x >= y else y # FURB136
| ^^^^^^^^^^^^^^^^^^ FURB136
7 |
8 | x if x < y else y # FURB136
|
= help: Replace with `max(x, y)`
Safe fix
3 3 |
4 4 | x if x > y else y # FURB136
5 5 |
6 |-x if x >= y else y # FURB136
6 |+max(x, y) # FURB136
7 7 |
8 8 | x if x < y else y # FURB136
9 9 |
FURB136.py:8:1: FURB136 [*] Replace `x if x < y else y` with `min(y, x)`
|
6 | x if x >= y else y # FURB136
7 |
8 | x if x < y else y # FURB136
| ^^^^^^^^^^^^^^^^^ FURB136
9 |
10 | x if x <= y else y # FURB136
|
= help: Replace with `min(y, x)`
Safe fix
5 5 |
6 6 | x if x >= y else y # FURB136
7 7 |
8 |-x if x < y else y # FURB136
8 |+min(y, x) # FURB136
9 9 |
10 10 | x if x <= y else y # FURB136
11 11 |
FURB136.py:10:1: FURB136 [*] Replace `x if x <= y else y` with `min(x, y)`
|
8 | x if x < y else y # FURB136
9 |
10 | x if x <= y else y # FURB136
| ^^^^^^^^^^^^^^^^^^ FURB136
11 |
12 | y if x > y else x # FURB136
|
= help: Replace with `min(x, y)`
Safe fix
7 7 |
8 8 | x if x < y else y # FURB136
9 9 |
10 |-x if x <= y else y # FURB136
10 |+min(x, y) # FURB136
11 11 |
12 12 | y if x > y else x # FURB136
13 13 |
FURB136.py:12:1: FURB136 [*] Replace `y if x > y else x` with `min(x, y)`
|
10 | x if x <= y else y # FURB136
11 |
12 | y if x > y else x # FURB136
| ^^^^^^^^^^^^^^^^^ FURB136
13 |
14 | y if x >= y else x # FURB136
|
= help: Replace with `min(x, y)`
Safe fix
9 9 |
10 10 | x if x <= y else y # FURB136
11 11 |
12 |-y if x > y else x # FURB136
12 |+min(x, y) # FURB136
13 13 |
14 14 | y if x >= y else x # FURB136
15 15 |
FURB136.py:14:1: FURB136 [*] Replace `y if x >= y else x` with `min(y, x)`
|
12 | y if x > y else x # FURB136
13 |
14 | y if x >= y else x # FURB136
| ^^^^^^^^^^^^^^^^^^ FURB136
15 |
16 | y if x < y else x # FURB136
|
= help: Replace with `min(y, x)`
Safe fix
11 11 |
12 12 | y if x > y else x # FURB136
13 13 |
14 |-y if x >= y else x # FURB136
14 |+min(y, x) # FURB136
15 15 |
16 16 | y if x < y else x # FURB136
17 17 |
FURB136.py:16:1: FURB136 [*] Replace `y if x < y else x` with `max(x, y)`
|
14 | y if x >= y else x # FURB136
15 |
16 | y if x < y else x # FURB136
| ^^^^^^^^^^^^^^^^^ FURB136
17 |
18 | y if x <= y else x # FURB136
|
= help: Replace with `max(x, y)`
Safe fix
13 13 |
14 14 | y if x >= y else x # FURB136
15 15 |
16 |-y if x < y else x # FURB136
16 |+max(x, y) # FURB136
17 17 |
18 18 | y if x <= y else x # FURB136
19 19 |
FURB136.py:18:1: FURB136 [*] Replace `y if x <= y else x` with `max(y, x)`
|
16 | y if x < y else x # FURB136
17 |
18 | y if x <= y else x # FURB136
| ^^^^^^^^^^^^^^^^^^ FURB136
19 |
20 | x + y if x > y else y # OK
|
= help: Replace with `max(y, x)`
Safe fix
15 15 |
16 16 | y if x < y else x # FURB136
17 17 |
18 |-y if x <= y else x # FURB136
18 |+max(y, x) # FURB136
19 19 |
20 20 | x + y if x > y else y # OK
21 21 |
FURB136.py:22:1: FURB136 [*] Replace `if` expression with `max(y, x)`
|
20 | x + y if x > y else y # OK
21 |
22 | / x if (
23 | | x
24 | | > y
25 | | ) else y # FURB136
| |________^ FURB136
|
= help: Replace with `max(y, x)`
Safe fix
19 19 |
20 20 | x + y if x > y else y # OK
21 21 |
22 |-x if (
23 |- x
24 |- > y
25 |-) else y # FURB136
22 |+max(y, x) # FURB136

View File

@@ -0,0 +1,67 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB152.py:3:5: FURB152 [*] Replace `3.14` with `math.pi`
|
1 | r = 3.1 # OK
2 |
3 | A = 3.14 * r ** 2 # FURB152
| ^^^^ FURB152
4 |
5 | C = 6.28 * r # FURB152
|
= help: Use `math.pi`
Safe fix
1 |+import math
1 2 | r = 3.1 # OK
2 3 |
3 |-A = 3.14 * r ** 2 # FURB152
4 |+A = math.pi * r ** 2 # FURB152
4 5 |
5 6 | C = 6.28 * r # FURB152
6 7 |
FURB152.py:5:5: FURB152 [*] Replace `6.28` with `math.tau`
|
3 | A = 3.14 * r ** 2 # FURB152
4 |
5 | C = 6.28 * r # FURB152
| ^^^^ FURB152
6 |
7 | e = 2.71 # FURB152
|
= help: Use `math.tau`
Safe fix
1 |+import math
1 2 | r = 3.1 # OK
2 3 |
3 4 | A = 3.14 * r ** 2 # FURB152
4 5 |
5 |-C = 6.28 * r # FURB152
6 |+C = math.tau * r # FURB152
6 7 |
7 8 | e = 2.71 # FURB152
FURB152.py:7:5: FURB152 [*] Replace `2.71` with `math.e`
|
5 | C = 6.28 * r # FURB152
6 |
7 | e = 2.71 # FURB152
| ^^^^ FURB152
|
= help: Use `math.e`
Safe fix
1 |+import math
1 2 | r = 3.1 # OK
2 3 |
3 4 | A = 3.14 * r ** 2 # FURB152
4 5 |
5 6 | C = 6.28 * r # FURB152
6 7 |
7 |-e = 2.71 # FURB152
8 |+e = math.e # FURB152

View File

@@ -1045,7 +1045,7 @@ mod tests {
#[test_case("match.py")]
fn control_flow_graph(filename: &str) {
let path = PathBuf::from_iter(["resources/test/fixtures/control-flow-graph", filename]);
let source = fs::read_to_string(&path).expect("failed to read file");
let source = fs::read_to_string(path).expect("failed to read file");
let stmts = parse(&source, Mode::Module, filename)
.unwrap_or_else(|err| panic!("failed to parse source: '{source}': {err}"))
.expect_module()

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