Compare commits

...

28 Commits

Author SHA1 Message Date
Charlie Marsh
d8162ce79d Bump version to 0.0.219 2023-01-11 23:46:01 -05:00
Charlie Marsh
e11ef54bda Improve globset documentation and help message (#1808)
Closes #1545.
2023-01-11 23:41:56 -05:00
messense
9a07b0623e Move top level ruff into python folder (#1806)
https://maturin.rs/project_layout.html#mixed-rustpython-project

Resolves #1805
2023-01-11 23:12:55 -05:00
Charlie Marsh
f450e2e79d Implement doc line length enforcement (#1804)
This PR implements `W505` (`DocLineTooLong`), which is similar to `E501`
(`LineTooLong`) but confined to doc lines.

I based the "doc line" definition on pycodestyle, which defines a doc
line as a standalone comment or string statement. Our definition is a
bit more liberal, since we consider any string statement a doc line
(even if it's part of a multi-line statement) -- but that seems fine to
me.

Note that, unusually, this rule requires custom extraction from both the
token stream (to find standalone comments) and the AST (to find string
statements).

Closes #1784.
2023-01-11 22:32:14 -05:00
Colin Delahunty
329946f162 Avoid erroneous Q002 error message for single-quote docstrings (#1777)
Fixes #1775. Before implementing your solution I thought of a slightly
simpler one. However, it will let this function pass:
```
def double_inside_single(a):
    'Double inside "single "'
```
If we want function to pass, my implementation works. But if we do not,
then I can go with how you suggested I implemented this (I left how I
would begin to handle it commented out). The bottom of the flake8-quotes
documentation seems to suggest that this should pass:
https://pypi.org/project/flake8-quotes/

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-01-11 20:01:54 -05:00
Charlie Marsh
588399e415 Fix Clippy error 2023-01-11 19:59:00 -05:00
Chammika Mannakkara
4523885268 flake8_simplify : SIM401 (#1778)
Ref #998 

- Implements SIM401 with fix
- Added tests

Notes: 
- only recognize simple ExprKind::Name variables in expr patterns for
now
- bug-fix from reference implementation: check 3-conditions (dict-key,
target-variable, dict-name) to be equal, `flake8_simplify` only test
first two (only first in second pattern)
2023-01-11 19:51:37 -05:00
Maksudul Haque
de81b0cd38 [flake8-simplify] Add Rule for SIM115 (Use context handler for opening files) (#1782)
ref: https://github.com/charliermarsh/ruff/issues/998

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-01-11 19:28:05 -05:00
Charlie Marsh
4fce296e3f Skip SIM108 violations for complex if-statements (#1802)
We now skip SIM108 violations if: the resulting statement would exceed
the user-specified line length, or the `if` statement contains comments.

Closes #1719.

Closes #1766.
2023-01-11 19:21:30 -05:00
Charlie Marsh
9d48d7bbd1 Skip unused argument checks for magic methods (#1801)
We still check `__init__`, `__call__`, and `__new__`.

Closes #1796.
2023-01-11 19:02:20 -05:00
Charlie Marsh
c56f263618 Avoid flagging builtins for OSError rewrites (#1800)
Related to (but does not fix) #1790.
2023-01-11 18:49:25 -05:00
Grzegorz Bokota
fb2382fbc3 Update readme to reflect #1763 (#1780)
When checking changes in the 0.0.218 release I noticed that auto fixing
PT004 and PT005 was disabled but this change was not reflected in
README. So I create this small PR to do this.

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-01-11 18:37:41 -05:00
Charlie Marsh
c92a5a8704 Avoid rewriting flake8-comprehensions expressions for builtin overrides (#1799)
Closes #1788.
2023-01-11 18:33:55 -05:00
Charlie Marsh
d7cf3147b7 Refactor flake8-comprehensions rules to take fewer arguments (#1797) 2023-01-11 18:21:18 -05:00
Charlie Marsh
bf4d35c705 Convert flake8-comprehensions checks to Checker style (#1795) 2023-01-11 18:11:20 -05:00
Charlie Marsh
4e97e9c7cf Improve PIE794 autofix behavior (#1794)
We now: (1) trigger PIE794 for objects without bases (not sure why this
was omitted before); and (2) remove the entire line, rather than leaving
behind trailing whitespace.

Resolves #1787.
2023-01-11 18:01:29 -05:00
Charlie Marsh
a3fcc3b28d Disable update check by default (#1786)
This has received enough criticism that I'm comfortable making it
opt-in.
2023-01-11 13:47:40 -05:00
Charlie Marsh
cfbd068dd5 Bump version to 0.0.218 2023-01-10 21:28:23 -05:00
Charlie Marsh
8aed23fe0a Avoid B023 false-positives for some common builtins (#1776)
This is based on the upstream work in
https://github.com/PyCQA/flake8-bugbear/pull/303 and
https://github.com/PyCQA/flake8-bugbear/pull/305/files.

Resolves #1686.
2023-01-10 21:23:48 -05:00
Colin Delahunty
c016c41c71 Pyupgrade: Format specifiers (#1594)
A part of #827. Posting this for visibility. Still has some work to do
to be done.

Things that still need done before this is ready:

- [x] Does not work when the item is being assigned to a variable
- [x] Does not work if being used in a function call
- [x] Fix incorrectly removed calls in the function
- [x] Has not been tested with pyupgrade negative test cases

Tests from pyupgrade can be seen here:
https://github.com/asottile/pyupgrade/blob/main/tests/features/format_literals_test.py

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-01-10 20:21:04 -05:00
Charlie Marsh
f1a5e53f06 Enable isort-style required-imports enforcement (#1762)
In isort, this is called `add-imports`, but I prefer the declarative
name.

The idea is that by adding the following to your `pyproject.toml`, you
can ensure that the import is included in all files:

```toml
[tool.ruff.isort]
required-imports = ["from __future__ import annotations"]
```

I mostly reverse-engineered isort's logic for making decisions, though I
made some slight tweaks that I think are preferable. A few comments:

- Like isort, we don't enforce this on empty files (like empty
`__init__.py`).
- Like isort, we require that the import is at the top-level.
- isort will skip any docstrings, and any comments on the first three
lines (I think, based on testing). Ruff places the import after the last
docstring or comment in the file preamble (that is: after the last
docstring or comment that comes before the _first_ non-docstring and
non-comment).

Resolves #1700.
2023-01-10 18:12:57 -05:00
Charlie Marsh
1e94e0221f Disable doctests (#1772)
We don't have any doctests, but `cargo test --all` spends more than half
the time on doctests? A little confusing, but this brings the test time
from > 4s to < 2s on my machine.
2023-01-10 15:10:16 -05:00
Martin Fischer
543865c96b Generate RuleCode::origin() via macro (#1770) 2023-01-10 13:20:43 -05:00
Maksudul Haque
b8e3f0bc13 [flake8-bandit] Add Rule for S508 (snmp insecure version) & S509 (snmp weak cryptography) (#1771)
ref: https://github.com/charliermarsh/ruff/issues/1646

Co-authored-by: messense <messense@icloud.com>
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-01-10 13:13:54 -05:00
Charlie Marsh
643cedb200 Move CONTRIBUTING.md to top-level (#1768) 2023-01-10 07:38:12 -05:00
Charlie Marsh
91620c378a Disable release builds on CI (#1761) 2023-01-10 07:33:03 -05:00
Harutaka Kawamura
b732135795 Do not autofix PT004 and PT005 (#1763)
As @edgarrmondragon commented in
https://github.com/charliermarsh/ruff/pull/1740#issuecomment-1376230550,
just renaming fixture doesn't work.
2023-01-10 07:24:16 -05:00
messense
9384a081f9 Implement flake8-simplify SIM112 (#1764)
Ref #998
2023-01-10 07:24:01 -05:00
128 changed files with 3529 additions and 1239 deletions

View File

@@ -27,8 +27,8 @@ jobs:
toolchain: nightly-2022-11-01
override: true
- uses: Swatinem/rust-cache@v1
- run: cargo build --all --release
- run: ./target/release/ruff_dev generate-all
- run: cargo build --all
- run: ./target/debug/ruff_dev generate-all
- run: git diff --quiet README.md || echo "::error file=README.md::This file is outdated. Run 'cargo +nightly dev generate-all'."
- run: git diff --quiet ruff.schema.json || echo "::error file=ruff.schema.json::This file is outdated. Run 'cargo +nightly dev generate-all'."
- run: git diff --exit-code -- README.md ruff.schema.json

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.217
rev: v0.0.219
hooks:
- id: ruff

8
Cargo.lock generated
View File

@@ -735,7 +735,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.217-dev.0"
version = "0.0.219-dev.0"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1874,7 +1874,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.217"
version = "0.0.219"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1942,7 +1942,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.217"
version = "0.0.219"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1962,7 +1962,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.217"
version = "0.0.219"
dependencies = [
"once_cell",
"proc-macro2",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.217"
version = "0.0.219"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -19,6 +19,7 @@ license = "MIT"
[lib]
name = "ruff"
crate-type = ["cdylib", "rlib"]
doctest = false
[dependencies]
annotate-snippets = { version = "0.9.1", features = ["color"] }
@@ -51,7 +52,7 @@ path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix
quick-junit = { version = "0.3.2" }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.217", path = "ruff_macros" }
ruff_macros = { version = "0.0.219", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "d532160333ffeb6dbeca2c2728c2391cd1e53b7f" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "d532160333ffeb6dbeca2c2728c2391cd1e53b7f" }

View File

@@ -180,7 +180,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.217'
rev: 'v0.0.219'
hooks:
- id: ruff
# Respect `exclude` and `extend-exclude` settings.
@@ -343,21 +343,21 @@ Options:
Disable cache reads
--isolated
Ignore all configuration files
--select <SELECT>
--select <RULE_CODE>
Comma-separated list of rule codes to enable (or ALL, to enable all rules)
--extend-select <EXTEND_SELECT>
--extend-select <RULE_CODE>
Like --select, but adds additional rule codes on top of the selected ones
--ignore <IGNORE>
--ignore <RULE_CODE>
Comma-separated list of rule codes to disable
--extend-ignore <EXTEND_IGNORE>
--extend-ignore <RULE_CODE>
Like --ignore, but adds additional rule codes on top of the ignored ones
--exclude <EXCLUDE>
--exclude <FILE_PATTERN>
List of paths, used to omit files and/or directories from analysis
--extend-exclude <EXTEND_EXCLUDE>
--extend-exclude <FILE_PATTERN>
Like --exclude, but adds additional files and directories on top of those already excluded
--fixable <FIXABLE>
--fixable <RULE_CODE>
List of rule codes to treat as eligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
--unfixable <UNFIXABLE>
--unfixable <RULE_CODE>
List of rule codes to treat as ineligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
--per-file-ignores <PER_FILE_IGNORES>
List of mappings from file pattern to code to exclude
@@ -597,6 +597,7 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI
| E902 | IOError | IOError: `...` | |
| E999 | SyntaxError | SyntaxError: `...` | |
| W292 | NoNewLineAtEndOfFile | No newline at end of file | 🛠 |
| W505 | DocLineTooLong | Doc line too long (89 > 88 characters) | |
| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | 🛠 |
### mccabe (C90)
@@ -614,6 +615,7 @@ For more, see [isort](https://pypi.org/project/isort/5.10.1/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| I001 | UnsortedImports | Import block is un-sorted or un-formatted | 🛠 |
| I002 | MissingRequiredImport | Missing required import: `from __future__ import ...` | 🛠 |
### pydocstyle (D)
@@ -701,6 +703,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| UP027 | RewriteListComprehension | Replace unpacked list comprehension with a generator expression | 🛠 |
| UP028 | RewriteYieldFrom | Replace `yield` over `for` loop with `yield from` | 🛠 |
| UP029 | UnnecessaryBuiltinImport | Unnecessary builtin import: `...` | 🛠 |
| UP030 | FormatLiterals | Use implicit references for positional format fields | 🛠 |
### pep8-naming (N)
@@ -777,6 +780,8 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/4.1.1/) on
| S324 | HashlibInsecureHashFunction | Probable use of insecure hash functions in `hashlib`: "..." | |
| S501 | RequestWithNoCertValidation | Probable use of `...` call with `verify=False` disabling SSL certificate checks | |
| S506 | UnsafeYAMLLoad | Probable use of unsafe `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. | |
| S508 | SnmpInsecureVersion | The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. | |
| S509 | SnmpWeakCryptography | You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. | |
### flake8-blind-except (BLE)
@@ -916,8 +921,8 @@ For more, see [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style
| PT001 | IncorrectFixtureParenthesesStyle | Use `@pytest.fixture()` over `@pytest.fixture` | 🛠 |
| PT002 | FixturePositionalArgs | Configuration for fixture `...` specified via positional args, use kwargs | |
| PT003 | ExtraneousScopeFunction | `scope='function'` is implied in `@pytest.fixture()` | |
| PT004 | MissingFixtureNameUnderscore | Fixture `...` does not return anything, add leading underscore | 🛠 |
| PT005 | IncorrectFixtureNameUnderscore | Fixture `...` returns a value, remove leading underscore | 🛠 |
| PT004 | MissingFixtureNameUnderscore | Fixture `...` does not return anything, add leading underscore | |
| PT005 | IncorrectFixtureNameUnderscore | Fixture `...` returns a value, remove leading underscore | |
| PT006 | ParametrizeNamesWrongType | Wrong name(s) type in `@pytest.mark.parametrize`, expected `tuple` | 🛠 |
| PT007 | ParametrizeValuesWrongType | Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple` | |
| PT008 | PatchWithLambda | Use `return_value=` instead of patching with `lambda` | |
@@ -971,6 +976,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| SIM115 | OpenFileWithContextHandler | Use context handler for opening files | |
| SIM101 | DuplicateIsinstanceCall | Multiple `isinstance` calls for `...`, merge into a single call | 🛠 |
| SIM102 | NestedIfStatements | Use a single `if` statement instead of nested `if` statements | |
| SIM103 | ReturnBoolConditionDirectly | Return the condition `...` directly | 🛠 |
@@ -980,6 +986,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/
| SIM109 | CompareWithTuple | Use `value in (..., ...)` instead of `value == ... or value == ...` | 🛠 |
| SIM110 | ConvertLoopToAny | Use `return any(x for x in y)` instead of `for` loop | 🛠 |
| SIM111 | ConvertLoopToAll | Use `return all(x for x in y)` instead of `for` loop | 🛠 |
| SIM112 | UseCapitalEnvironmentVariables | Use capitalized environment variable `...` instead of `...` | 🛠 |
| SIM117 | MultipleWithStatements | Use a single `with` statement with multiple contexts instead of nested `with` statements | |
| SIM118 | KeyInDict | Use `key in dict` instead of `key in dict.keys()` | 🛠 |
| SIM201 | NegateEqualOp | Use `left != right` instead of `not left == right` | 🛠 |
@@ -993,6 +1000,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/
| SIM222 | OrTrue | Use `True` instead of `... or True` | 🛠 |
| SIM223 | AndFalse | Use `False` instead of `... and False` | 🛠 |
| SIM300 | YodaConditions | Yoda conditions are discouraged, use `left == right` instead | 🛠 |
| SIM401 | DictGetWithDefault | Use `var = dict.get(key, "default")` instead of an `if` block | 🛠 |
### flake8-tidy-imports (TID)
@@ -1817,6 +1825,8 @@ Exclusions are based on globs, and can be either:
`directory`). Note that these paths are relative to the project root
(e.g., the directory containing your `pyproject.toml`).
For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
Note that you'll typically want to use
[`extend-exclude`](#extend-exclude) to modify the excluded paths.
@@ -1864,6 +1874,18 @@ line-length = 100
A list of file patterns to omit from linting, in addition to those
specified by `exclude`.
Exclusions are based on globs, and can be either:
- Single-path patterns, like `.mypy_cache` (to exclude any directory
named `.mypy_cache` in the tree), `foo.py` (to exclude any file named
`foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ).
- Relative patterns, like `directory/foo.py` (to exclude that specific
file) or `directory/*.py` (to exclude any Python files in
`directory`). Note that these paths are relative to the project root
(e.g., the directory containing your `pyproject.toml`).
For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
**Default value**: `[]`
**Type**: `Vec<FilePattern>`
@@ -2335,7 +2357,7 @@ unfixable = ["F401"]
Enable or disable automatic update checks (overridden by the
`--update-check` and `--no-update-check` command-line flags).
**Default value**: `true`
**Default value**: `false`
**Type**: `bool`
@@ -2343,7 +2365,7 @@ Enable or disable automatic update checks (overridden by the
```toml
[tool.ruff]
update-check = false
update-check = true
```
---
@@ -2998,6 +3020,23 @@ order-by-type = true
---
#### [`required-imports`](#required-imports)
Add the specified import line to all files.
**Default value**: `[]`
**Type**: `Vec<String>`
**Example usage**:
```toml
[tool.ruff.isort]
add-import = ["from __future__ import annotations"]
```
---
#### [`single-line-exclusions`](#single-line-exclusions)
One or more modules to exclude from the single line rule.
@@ -3137,6 +3176,24 @@ ignore-overlong-task-comments = true
---
#### [`max-doc-length`](#max-doc-length)
The maximum line length to allow for line-length violations within
documentation (`W505`), including standalone comments.
**Default value**: `None`
**Type**: `usize`
**Example usage**:
```toml
[tool.ruff.pycodestyle]
max-doc-length = 88
```
---
### `pydocstyle`
#### [`convention`](#convention)
@@ -3191,4 +3248,4 @@ MIT
## Contributing
Contributions are welcome and hugely appreciated. To get started, check out the
[contributing guidelines](https://github.com/charliermarsh/ruff/blob/main/.github/CONTRIBUTING.md).
[contributing guidelines](https://github.com/charliermarsh/ruff/blob/main/CONTRIBUTING.md).

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.217"
version = "0.0.219"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.217"
version = "0.0.219"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,10 +1,11 @@
[package]
name = "flake8-to-ruff"
version = "0.0.217-dev.0"
version = "0.0.219-dev.0"
edition = "2021"
[lib]
name = "flake8_to_ruff"
doctest = false
[dependencies]
anyhow = { version = "1.0.66" }

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.217"
version = "0.0.219"
description = "An extremely fast Python linter, written in Rust."
authors = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },
@@ -35,6 +35,7 @@ urls = { repository = "https://github.com/charliermarsh/ruff" }
[tool.maturin]
bindings = "bin"
python-source = "python"
strip = true
[tool.setuptools]

View File

@@ -0,0 +1,6 @@
from pysnmp.hlapi import CommunityData
CommunityData("public", mpModel=0) # S508
CommunityData("public", mpModel=1) # S508
CommunityData("public", mpModel=2) # OK

View File

@@ -0,0 +1,7 @@
from pysnmp.hlapi import UsmUserData
insecure = UsmUserData("securityName") # S509
auth_no_priv = UsmUserData("securityName", "authName") # S509
less_insecure = UsmUserData("securityName", "authName", "privName") # OK

View File

@@ -25,10 +25,10 @@ for x in range(3):
def check_inside_functions_too():
ls = [lambda: x for x in range(2)]
st = {lambda: x for x in range(2)}
gn = (lambda: x for x in range(2))
dt = {x: lambda: x for x in range(2)}
ls = [lambda: x for x in range(2)] # error
st = {lambda: x for x in range(2)} # error
gn = (lambda: x for x in range(2)) # error
dt = {x: lambda: x for x in range(2)} # error
async def pointless_async_iterable():
@@ -37,9 +37,9 @@ async def pointless_async_iterable():
async def container_for_problems():
async for x in pointless_async_iterable():
functions.append(lambda: x)
functions.append(lambda: x) # error
[lambda: x async for x in pointless_async_iterable()]
[lambda: x async for x in pointless_async_iterable()] # error
a = 10
@@ -47,10 +47,10 @@ b = 0
while True:
a = a_ = a - 1
b += 1
functions.append(lambda: a)
functions.append(lambda: a_)
functions.append(lambda: b)
functions.append(lambda: c) # not a name error because of late binding!
functions.append(lambda: a) # error
functions.append(lambda: a_) # error
functions.append(lambda: b) # error
functions.append(lambda: c) # error, but not a name error due to late binding
c: bool = a > 3
if not c:
break
@@ -58,7 +58,7 @@ while True:
# Nested loops should not duplicate reports
for j in range(2):
for k in range(3):
lambda: j * k
lambda: j * k # error
for j, k, l in [(1, 2, 3)]:
@@ -80,3 +80,95 @@ for var in range(2):
for i in range(3):
lambda: f"{i}"
# `query` is defined in the function, so also defining it in the loop should be OK.
for name in ["a", "b"]:
query = name
def myfunc(x):
query = x
query_post = x
_ = query
_ = query_post
query_post = name # in case iteration order matters
# Bug here because two dict comprehensions reference `name`, one of which is inside
# the lambda. This should be totally fine, of course.
_ = {
k: v
for k, v in reduce(
lambda data, event: merge_mappings(
[data, {name: f(caches, data, event) for name, f in xx}]
),
events,
{name: getattr(group, name) for name in yy},
).items()
if k in backfill_fields
}
# OK to define lambdas if they're immediately consumed, typically as the `key=`
# argument or in a consumed `filter()` (even if a comprehension is better style)
for x in range(2):
# It's not a complete get-out-of-linting-free construct - these should fail:
min([None, lambda: x], key=repr)
sorted([None, lambda: x], key=repr)
any(filter(bool, [None, lambda: x]))
list(filter(bool, [None, lambda: x]))
all(reduce(bool, [None, lambda: x]))
# But all these should be OK:
min(range(3), key=lambda y: x * y)
max(range(3), key=lambda y: x * y)
sorted(range(3), key=lambda y: x * y)
any(map(lambda y: x < y, range(3)))
all(map(lambda y: x < y, range(3)))
set(map(lambda y: x < y, range(3)))
list(map(lambda y: x < y, range(3)))
tuple(map(lambda y: x < y, range(3)))
sorted(map(lambda y: x < y, range(3)))
frozenset(map(lambda y: x < y, range(3)))
any(filter(lambda y: x < y, range(3)))
all(filter(lambda y: x < y, range(3)))
set(filter(lambda y: x < y, range(3)))
list(filter(lambda y: x < y, range(3)))
tuple(filter(lambda y: x < y, range(3)))
sorted(filter(lambda y: x < y, range(3)))
frozenset(filter(lambda y: x < y, range(3)))
any(reduce(lambda y: x | y, range(3)))
all(reduce(lambda y: x | y, range(3)))
set(reduce(lambda y: x | y, range(3)))
list(reduce(lambda y: x | y, range(3)))
tuple(reduce(lambda y: x | y, range(3)))
sorted(reduce(lambda y: x | y, range(3)))
frozenset(reduce(lambda y: x | y, range(3)))
import functools
any(functools.reduce(lambda y: x | y, range(3)))
all(functools.reduce(lambda y: x | y, range(3)))
set(functools.reduce(lambda y: x | y, range(3)))
list(functools.reduce(lambda y: x | y, range(3)))
tuple(functools.reduce(lambda y: x | y, range(3)))
sorted(functools.reduce(lambda y: x | y, range(3)))
frozenset(functools.reduce(lambda y: x | y, range(3)))
# OK because the lambda which references a loop variable is defined in a `return`
# statement, and after we return the loop variable can't be redefined.
# In principle we could do something fancy with `break`, but it's not worth it.
def iter_f(names):
for name in names:
if exists(name):
return lambda: name if exists(name) else None
if foo(name):
return [lambda: name] # known false alarm
if False:
return [lambda: i for i in range(3)] # error

View File

@@ -2,3 +2,10 @@ x = list(x for x in range(3))
x = list(
x for x in range(3)
)
def list(*args, **kwargs):
return None
list(x for x in range(3))

View File

@@ -2,3 +2,10 @@ x = set(x for x in range(3))
x = set(
x for x in range(3)
)
def set(*args, **kwargs):
return None
set(x for x in range(3))

View File

@@ -3,3 +3,10 @@ l = list()
d1 = dict()
d2 = dict(a=1)
d3 = dict(**d2)
def list():
return [1, 2, 3]
a = list()

View File

@@ -4,3 +4,10 @@ list(sorted(x))
reversed(sorted(x))
reversed(sorted(x, key=lambda e: e))
reversed(sorted(x, reverse=True))
def reversed(*args, **kwargs):
return None
reversed(sorted(x, reverse=True))

View File

@@ -31,3 +31,10 @@ class User(BaseModel):
@buzz.setter
def buzz(self, value: str | int) -> None:
...
class User:
bar: str = StringField()
foo: bool = BooleanField()
# ...
bar = StringField() # PIE794

View File

@@ -17,6 +17,15 @@ def fun_with_params_no_docstring(a, b="""
""" """docstring"""):
pass
def fun_with_params_no_docstring2(a, b=c[foo():], c=\
""" not a docstring """):
pass
def function_with_single_docstring(a):
"Single line docstring"
def double_inside_single(a):
'Double inside "single "'

View File

@@ -13,11 +13,19 @@ def foo2():
def fun_with_params_no_docstring(a, b='''
not a
not a
''' '''docstring'''):
pass
def fun_with_params_no_docstring2(a, b=c[foo():], c=\
''' not a docstring '''):
pass
def function_with_single_docstring(a):
'Single line docstring'
def double_inside_single(a):
"Double inside 'single '"

View File

@@ -1,13 +1,13 @@
# Bad
# SIM108
if a:
b = c
else:
b = d
# Good
# OK
b = c if a else d
# https://github.com/MartinThoma/flake8-simplify/issues/115
# OK
if a:
b = c
elif c:
@@ -15,6 +15,7 @@ elif c:
else:
b = d
# OK
if True:
pass
elif a:
@@ -22,6 +23,7 @@ elif a:
else:
b = 2
# OK (false negative)
if True:
pass
else:
@@ -30,19 +32,62 @@ else:
else:
b = 2
import sys
# OK
if sys.version_info >= (3, 9):
randbytes = random.randbytes
else:
randbytes = _get_random_bytes
# OK
if sys.platform == "darwin":
randbytes = random.randbytes
else:
randbytes = _get_random_bytes
# OK
if sys.platform.startswith("linux"):
randbytes = random.randbytes
else:
randbytes = _get_random_bytes
# OK (includes comments)
if x > 0:
# test test
abc = x
else:
# test test test
abc = -x
# OK (too long)
if parser.errno == BAD_FIRST_LINE:
req = wrappers.Request(sock, server=self._server)
else:
req = wrappers.Request(
sock,
parser.get_method(),
parser.get_scheme() or _scheme,
parser.get_path(),
parser.get_version(),
parser.get_query_string(),
server=self._server,
)
# SIM108
if a:
b = cccccccccccccccccccccccccccccccccccc
else:
b = ddddddddddddddddddddddddddddddddddddd
# OK (too long)
if True:
if a:
b = cccccccccccccccccccccccccccccccccccc
else:
b = ddddddddddddddddddddddddddddddddddddd

View File

@@ -0,0 +1,19 @@
import os
# Bad
os.environ['foo']
os.environ.get('foo')
os.environ.get('foo', 'bar')
os.getenv('foo')
# Good
os.environ['FOO']
os.environ.get('FOO')
os.environ.get('FOO', 'bar')
os.getenv('FOO')

View File

@@ -0,0 +1,6 @@
f = open('foo.txt') # SIM115
data = f.read()
f.close()
with open('foo.txt') as f: # OK
data = f.read()

View File

@@ -0,0 +1,81 @@
###
# Positive cases
###
# SIM401 (pattern-1)
if key in a_dict:
var = a_dict[key]
else:
var = "default1"
# SIM401 (pattern-2)
if key not in a_dict:
var = "default2"
else:
var = a_dict[key]
# SIM401 (default with a complex expression)
if key in a_dict:
var = a_dict[key]
else:
var = val1 + val2
# SIM401 (complex expression in key)
if keys[idx] in a_dict:
var = a_dict[keys[idx]]
else:
var = "default"
# SIM401 (complex expression in dict)
if key in dicts[idx]:
var = dicts[idx][key]
else:
var = "default"
# SIM401 (complex expression in var)
if key in a_dict:
vars[idx] = a_dict[key]
else:
vars[idx] = "default"
###
# Negative cases
###
# OK (false negative)
if not key in a_dict:
var = "default"
else:
var = a_dict[key]
# OK (different dict)
if key in a_dict:
var = other_dict[key]
else:
var = "default"
# OK (different key)
if key in a_dict:
var = a_dict[other_key]
else:
var = "default"
# OK (different var)
if key in a_dict:
var = a_dict[key]
else:
other_var = "default"
# OK (extra vars in body)
if key in a_dict:
var = a_dict[key]
var2 = value2
else:
var = "default"
# OK (extra vars in orelse)
if key in a_dict:
var = a_dict[key]
else:
var2 = value2
var = "default"

View File

@@ -181,3 +181,17 @@ def f(a: int, b: int) -> str:
def f(a, b):
return f"{a}{b}"
###
# Unused arguments on magic methods.
###
class C:
def __init__(self, x) -> None:
print("Hello, world!")
def __str__(self) -> str:
return "Hello, world!"
def __exit__(self, exc_type, exc_value, traceback) -> None:
print("Hello, world!")

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env python3
x = 1

View File

@@ -0,0 +1,3 @@
"""Hello, world!"""
x = 1

View File

@@ -0,0 +1 @@
"""Hello, world!"""

View File

@@ -0,0 +1,2 @@
"""Hello, world!"""; x = \
1; y = 2

View File

@@ -0,0 +1 @@
"""Hello, world!"""; x = 1

View File

@@ -0,0 +1,2 @@
from __future__ import generator_stop
import os

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env python3
"""Here's a top-level docstring that's over the limit."""
def f():
"""Here's a docstring that's also over the limit."""
x = 1 # Here's a comment that's over the limit, but it's not standalone.
# Here's a standalone comment that's over the limit.
print("Here's a string that's over the limit, but it's not a docstring.")
"This is also considered a docstring, and is over the limit."

View File

@@ -0,0 +1,8 @@
class SocketError(Exception):
pass
try:
raise SocketError()
except SocketError:
pass

View File

@@ -0,0 +1,36 @@
# Invalid calls; errors expected.
"{0}" "{1}" "{2}".format(1, 2, 3)
"a {3} complicated {1} string with {0} {2}".format(
"first", "second", "third", "fourth"
)
'{0}'.format(1)
'{0:x}'.format(30)
x = '{0}'.format(1)
'''{0}\n{1}\n'''.format(1, 2)
x = "foo {0}" \
"bar {1}".format(1, 2)
("{0}").format(1)
"\N{snowman} {0}".format(1)
'{' '0}'.format(1)
# These will not change because we are waiting for libcst to fix this issue:
# https://github.com/Instagram/LibCST/issues/846
print(
'foo{0}'
'bar{1}'.format(1, 2)
)
print(
'foo{0}' # ohai\n"
'bar{1}'.format(1, 2)
)

View File

@@ -0,0 +1,23 @@
# Valid calls; no errors expected.
'{}'.format(1)
x = ('{0} {1}',)
'{0} {0}'.format(1)
'{0:<{1}}'.format(1, 4)
f"{0}".format(a)
f"{0}".format(1)
print(f"{0}".format(1))
# I did not include the following tests because ruff does not seem to work with
# invalid python syntax (which is a good thing)
# "{0}"format(1)
# '{'.format(1)", "'}'.format(1)
# ("{0}" # {1}\n"{2}").format(1, 2, 3)

View File

@@ -40,7 +40,7 @@
]
},
"exclude": {
"description": "A list of file patterns to exclude from linting.\n\nExclusions are based on globs, and can be either:\n\n- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`).\n\nNote that you'll typically want to use [`extend-exclude`](#extend-exclude) to modify the excluded paths.",
"description": "A list of file patterns to exclude from linting.\n\nExclusions are based on globs, and can be either:\n\n- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`).\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).\n\nNote that you'll typically want to use [`extend-exclude`](#extend-exclude) to modify the excluded paths.",
"type": [
"array",
"null"
@@ -57,7 +57,7 @@
]
},
"extend-exclude": {
"description": "A list of file patterns to omit from linting, in addition to those specified by `exclude`.",
"description": "A list of file patterns to omit from linting, in addition to those specified by `exclude`.\n\nExclusions are based on globs, and can be either:\n\n- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`).\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).",
"type": [
"array",
"null"
@@ -820,6 +820,16 @@
"null"
]
},
"required-imports": {
"description": "Add the specified import line to all files.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"single-line-exclusions": {
"description": "One or more modules to exclude from the single line rule.",
"type": [
@@ -935,6 +945,15 @@
"boolean",
"null"
]
},
"max-doc-length": {
"description": "The maximum line length to allow for line-length violations within documentation (`W505`), including standalone comments.",
"type": [
"integer",
"null"
],
"format": "uint",
"minimum": 0.0
}
},
"additionalProperties": false
@@ -1268,6 +1287,7 @@
"I0",
"I00",
"I001",
"I002",
"I2",
"I25",
"I252",
@@ -1493,6 +1513,8 @@
"S50",
"S501",
"S506",
"S508",
"S509",
"SIM",
"SIM1",
"SIM10",
@@ -1506,6 +1528,8 @@
"SIM11",
"SIM110",
"SIM111",
"SIM112",
"SIM115",
"SIM117",
"SIM118",
"SIM2",
@@ -1525,6 +1549,9 @@
"SIM3",
"SIM30",
"SIM300",
"SIM4",
"SIM40",
"SIM401",
"T",
"T1",
"T10",
@@ -1592,10 +1619,15 @@
"UP027",
"UP028",
"UP029",
"UP03",
"UP030",
"W",
"W2",
"W29",
"W292",
"W5",
"W50",
"W505",
"W6",
"W60",
"W605",

View File

@@ -1,8 +1,12 @@
[package]
name = "ruff_dev"
version = "0.0.217"
version = "0.0.219"
edition = "2021"
[lib]
name = "ruff_dev"
doctest = false
[dependencies]
anyhow = { version = "1.0.66" }
clap = { version = "4.0.1", features = ["derive"] }

View File

@@ -1,10 +1,11 @@
[package]
name = "ruff_macros"
version = "0.0.217"
version = "0.0.219"
edition = "2021"
[lib]
proc-macro = true
doctest = false
[dependencies]
once_cell = { version = "1.17.0" }

View File

@@ -12,9 +12,12 @@
)]
#![forbid(unsafe_code)]
use syn::{parse_macro_input, DeriveInput};
use proc_macro2::Span;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Ident};
mod config;
mod prefixes;
mod rule_code_prefix;
#[proc_macro_derive(ConfigurationOptions, attributes(option, doc, option_group))]
@@ -34,3 +37,23 @@ pub fn derive_rule_code_prefix(input: proc_macro::TokenStream) -> proc_macro::To
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro]
pub fn origin_by_code(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ident = parse_macro_input!(item as Ident).to_string();
let mut iter = prefixes::PREFIX_TO_ORIGIN.iter();
let origin = loop {
let (prefix, origin) = iter
.next()
.unwrap_or_else(|| panic!("code doesn't start with any recognized prefix: {ident}"));
if ident.starts_with(prefix) {
break origin;
}
};
let prefix = Ident::new(origin, Span::call_site());
quote! {
RuleOrigin::#prefix
}
.into()
}

View File

@@ -0,0 +1,53 @@
// Longer prefixes should come first so that you can find an origin for a code
// by simply picking the first entry that starts with the given prefix.
pub const PREFIX_TO_ORIGIN: &[(&str, &str)] = &[
("ANN", "Flake8Annotations"),
("ARG", "Flake8UnusedArguments"),
("A", "Flake8Builtins"),
("BLE", "Flake8BlindExcept"),
("B", "Flake8Bugbear"),
("C4", "Flake8Comprehensions"),
("C9", "McCabe"),
("DTZ", "Flake8Datetimez"),
("D", "Pydocstyle"),
("ERA", "Eradicate"),
("EM", "Flake8ErrMsg"),
("E", "Pycodestyle"),
("FBT", "Flake8BooleanTrap"),
("F", "Pyflakes"),
("ICN", "Flake8ImportConventions"),
("ISC", "Flake8ImplicitStrConcat"),
("I", "Isort"),
("N", "PEP8Naming"),
("PD", "PandasVet"),
("PGH", "PygrepHooks"),
("PL", "Pylint"),
("PT", "Flake8PytestStyle"),
("Q", "Flake8Quotes"),
("RET", "Flake8Return"),
("SIM", "Flake8Simplify"),
("S", "Flake8Bandit"),
("T10", "Flake8Debugger"),
("T20", "Flake8Print"),
("TID", "Flake8TidyImports"),
("UP", "Pyupgrade"),
("W", "Pycodestyle"),
("YTT", "Flake82020"),
("PIE", "Flake8Pie"),
("RUF", "Ruff"),
];
#[cfg(test)]
mod tests {
use super::PREFIX_TO_ORIGIN;
#[test]
fn order() {
for (idx, (prefix, _)) in PREFIX_TO_ORIGIN.iter().enumerate() {
for (prior_prefix, _) in PREFIX_TO_ORIGIN[..idx].iter() {
assert!(!prefix.starts_with(prior_prefix));
}
}
}
}

View File

@@ -116,33 +116,6 @@ impl Violation for %s {
fp.write("\n")
has_written = True
# Add the relevant code-to-origin pair to `src/registry.rs`.
with open(os.path.join(ROOT_DIR, "src/registry.rs")) as fp:
content = fp.read()
seen_impl = False
has_written = False
with open(os.path.join(ROOT_DIR, "src/registry.rs"), "w") as fp:
for line in content.splitlines():
fp.write(line)
fp.write("\n")
if has_written:
continue
if line.startswith("impl RuleCode"):
seen_impl = True
continue
if not seen_impl:
continue
if line.strip() == f"// {origin}":
indent = line.split("//")[0]
fp.write(f"{indent}RuleCode::{code} => RuleOrigin::{pascal_case(origin)},")
fp.write("\n")
has_written = True
if __name__ == "__main__":
parser = argparse.ArgumentParser(

View File

@@ -1,5 +1,12 @@
use rustpython_ast::{Expr, Stmt, StmtKind};
pub fn name(stmt: &Stmt) -> &str {
match &stmt.node {
StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => name,
_ => panic!("Expected StmtKind::FunctionDef | StmtKind::AsyncFunctionDef"),
}
}
pub fn decorator_list(stmt: &Stmt) -> &Vec<Expr> {
match &stmt.node {
StmtKind::FunctionDef { decorator_list, .. }

View File

@@ -388,6 +388,12 @@ impl<'a> From<&'a Box<Expr>> for Box<ComparableExpr<'a>> {
}
}
impl<'a> From<&'a Box<Expr>> for ComparableExpr<'a> {
fn from(expr: &'a Box<Expr>) -> Self {
(&**expr).into()
}
}
impl<'a> From<&'a Expr> for ComparableExpr<'a> {
fn from(expr: &'a Expr) -> Self {
match &expr.node {

View File

@@ -430,6 +430,13 @@ pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> {
arg_names
}
/// Returns `true` if a statement or expression includes at least one comment.
pub fn has_comments<T>(located: &Located<T>, locator: &SourceCodeLocator) -> bool {
lexer::make_tokenizer(&locator.slice_source_code_range(&Range::from_located(located)))
.flatten()
.any(|(_, tok, _)| matches!(tok, Tok::Comment(..)))
}
/// Returns `true` if a call is an argumented `super` invocation.
pub fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
if let ExprKind::Name { id, .. } = &func.node {
@@ -714,6 +721,21 @@ pub fn followed_by_multi_statement_line(stmt: &Stmt, locator: &SourceCodeLocator
match_trailing_content(stmt, locator)
}
/// Return `true` if a `Stmt` is a docstring.
pub fn is_docstring_stmt(stmt: &Stmt) -> bool {
if let StmtKind::Expr { value } = &stmt.node {
matches!(
value.node,
ExprKind::Constant {
value: Constant::Str { .. },
..
}
)
} else {
false
}
}
#[derive(Default)]
/// A simple representation of a call's positional and keyword arguments.
pub struct SimpleCallArgs<'a> {
@@ -759,6 +781,11 @@ impl<'a> SimpleCallArgs<'a> {
}
None
}
/// Get the number of positional and keyword arguments used.
pub fn len(&self) -> usize {
self.args.len() + self.kwargs.len()
}
}
#[cfg(test)]

View File

@@ -658,7 +658,7 @@ where
}
if self.settings.enabled.contains(&RuleCode::PIE794) {
flake8_pie::rules::dupe_class_field_definitions(self, bases, body);
flake8_pie::rules::dupe_class_field_definitions(self, stmt, body);
}
self.check_builtin_shadowing(name, stmt, false);
@@ -1207,14 +1207,14 @@ where
}
if self.settings.enabled.contains(&RuleCode::UP024) {
if let Some(item) = exc {
pyupgrade::rules::os_error_alias(self, item);
pyupgrade::rules::os_error_alias(self, &item);
}
}
}
StmtKind::AugAssign { target, .. } => {
self.handle_node_load(target);
}
StmtKind::If { test, .. } => {
StmtKind::If { test, body, orelse } => {
if self.settings.enabled.contains(&RuleCode::F634) {
pyflakes::rules::if_tuple(self, stmt, test);
}
@@ -1231,6 +1231,11 @@ where
self.current_stmt_parent().map(|parent| parent.0),
);
}
if self.settings.enabled.contains(&RuleCode::SIM401) {
flake8_simplify::rules::use_dict_get_with_default(
self, stmt, test, body, orelse,
);
}
}
StmtKind::Assert { test, msg } => {
if self.settings.enabled.contains(&RuleCode::F631) {
@@ -1333,7 +1338,7 @@ where
flake8_bugbear::rules::redundant_tuple_in_exception_handler(self, handlers);
}
if self.settings.enabled.contains(&RuleCode::UP024) {
pyupgrade::rules::os_error_alias(self, handlers);
pyupgrade::rules::os_error_alias(self, &handlers);
}
if self.settings.enabled.contains(&RuleCode::PT017) {
self.diagnostics.extend(
@@ -1405,6 +1410,9 @@ where
if self.settings.enabled.contains(&RuleCode::B015) {
flake8_bugbear::rules::useless_comparison(self, value);
}
if self.settings.enabled.contains(&RuleCode::SIM112) {
flake8_simplify::rules::use_capital_environment_variables(self, value);
}
}
_ => {}
}
@@ -1827,6 +1835,8 @@ where
|| self.settings.enabled.contains(&RuleCode::F523)
|| self.settings.enabled.contains(&RuleCode::F524)
|| self.settings.enabled.contains(&RuleCode::F525)
// pyupgrade
|| self.settings.enabled.contains(&RuleCode::UP030)
{
if let ExprKind::Attribute { value, attr, .. } = &func.node {
if let ExprKind::Constant {
@@ -1873,6 +1883,10 @@ where
self, &summary, location,
);
}
if self.settings.enabled.contains(&RuleCode::UP030) {
pyupgrade::rules::format_literals(self, &summary, expr);
}
}
}
}
@@ -1912,7 +1926,7 @@ where
pyupgrade::rules::replace_stdout_stderr(self, expr, keywords);
}
if self.settings.enabled.contains(&RuleCode::UP024) {
pyupgrade::rules::os_error_alias(self, expr);
pyupgrade::rules::os_error_alias(self, &expr);
}
// flake8-print
@@ -1988,6 +2002,28 @@ where
self.diagnostics.push(diagnostic);
}
}
if self.settings.enabled.contains(&RuleCode::S508) {
if let Some(diagnostic) = flake8_bandit::rules::snmp_insecure_version(
func,
args,
keywords,
&self.from_imports,
&self.import_aliases,
) {
self.diagnostics.push(diagnostic);
}
}
if self.settings.enabled.contains(&RuleCode::S509) {
if let Some(diagnostic) = flake8_bandit::rules::snmp_weak_cryptography(
func,
args,
keywords,
&self.from_imports,
&self.import_aliases,
) {
self.diagnostics.push(diagnostic);
}
}
if self.settings.enabled.contains(&RuleCode::S106) {
self.diagnostics
.extend(flake8_bandit::rules::hardcoded_password_func_arg(keywords));
@@ -2017,205 +2053,75 @@ where
// flake8-comprehensions
if self.settings.enabled.contains(&RuleCode::C400) {
if let Some(diagnostic) =
flake8_comprehensions::rules::unnecessary_generator_list(
expr,
func,
args,
keywords,
self.locator,
self.patch(&RuleCode::C400),
Range::from_located(expr),
)
{
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_generator_list(
self, expr, func, args, keywords,
);
}
if self.settings.enabled.contains(&RuleCode::C401) {
if let Some(diagnostic) =
flake8_comprehensions::rules::unnecessary_generator_set(
expr,
func,
args,
keywords,
self.locator,
self.patch(&RuleCode::C401),
Range::from_located(expr),
)
{
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_generator_set(
self, expr, func, args, keywords,
);
}
if self.settings.enabled.contains(&RuleCode::C402) {
if let Some(diagnostic) =
flake8_comprehensions::rules::unnecessary_generator_dict(
expr,
func,
args,
keywords,
self.locator,
self.patch(&RuleCode::C402),
Range::from_located(expr),
)
{
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_generator_dict(
self, expr, func, args, keywords,
);
}
if self.settings.enabled.contains(&RuleCode::C403) {
if let Some(diagnostic) =
flake8_comprehensions::rules::unnecessary_list_comprehension_set(
expr,
func,
args,
keywords,
self.locator,
self.patch(&RuleCode::C403),
Range::from_located(expr),
)
{
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_list_comprehension_set(
self, expr, func, args, keywords,
);
}
if self.settings.enabled.contains(&RuleCode::C404) {
if let Some(diagnostic) =
flake8_comprehensions::rules::unnecessary_list_comprehension_dict(
expr,
func,
args,
keywords,
self.locator,
self.patch(&RuleCode::C404),
Range::from_located(expr),
)
{
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_list_comprehension_dict(
self, expr, func, args, keywords,
);
}
if self.settings.enabled.contains(&RuleCode::C405) {
if let Some(diagnostic) = flake8_comprehensions::rules::unnecessary_literal_set(
expr,
func,
args,
keywords,
self.locator,
self.patch(&RuleCode::C405),
Range::from_located(expr),
) {
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_literal_set(
self, expr, func, args, keywords,
);
}
if self.settings.enabled.contains(&RuleCode::C406) {
if let Some(diagnostic) = flake8_comprehensions::rules::unnecessary_literal_dict(
expr,
func,
args,
keywords,
self.locator,
self.patch(&RuleCode::C406),
Range::from_located(expr),
) {
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_literal_dict(
self, expr, func, args, keywords,
);
}
if self.settings.enabled.contains(&RuleCode::C408) {
if let Some(diagnostic) =
flake8_comprehensions::rules::unnecessary_collection_call(
expr,
func,
args,
keywords,
self.locator,
self.patch(&RuleCode::C408),
Range::from_located(expr),
)
{
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_collection_call(
self, expr, func, args, keywords,
);
}
if self.settings.enabled.contains(&RuleCode::C409) {
if let Some(diagnostic) =
flake8_comprehensions::rules::unnecessary_literal_within_tuple_call(
expr,
func,
args,
self.locator,
self.patch(&RuleCode::C409),
Range::from_located(expr),
)
{
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_literal_within_tuple_call(
self, expr, func, args,
);
}
if self.settings.enabled.contains(&RuleCode::C410) {
if let Some(diagnostic) =
flake8_comprehensions::rules::unnecessary_literal_within_list_call(
expr,
func,
args,
self.locator,
self.patch(&RuleCode::C410),
Range::from_located(expr),
)
{
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_literal_within_list_call(
self, expr, func, args,
);
}
if self.settings.enabled.contains(&RuleCode::C411) {
if let Some(diagnostic) = flake8_comprehensions::rules::unnecessary_list_call(
expr,
func,
args,
self.locator,
self.patch(&RuleCode::C411),
Range::from_located(expr),
) {
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_list_call(self, expr, func, args);
}
if self.settings.enabled.contains(&RuleCode::C413) {
if let Some(diagnostic) =
flake8_comprehensions::rules::unnecessary_call_around_sorted(
expr,
func,
args,
self.locator,
self.patch(&RuleCode::C413),
Range::from_located(expr),
)
{
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_call_around_sorted(
self, expr, func, args,
);
}
if self.settings.enabled.contains(&RuleCode::C414) {
if let Some(diagnostic) =
flake8_comprehensions::rules::unnecessary_double_cast_or_process(
func,
args,
Range::from_located(expr),
)
{
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_double_cast_or_process(
self, expr, func, args,
);
}
if self.settings.enabled.contains(&RuleCode::C415) {
if let Some(diagnostic) =
flake8_comprehensions::rules::unnecessary_subscript_reversal(
func,
args,
Range::from_located(expr),
)
{
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_subscript_reversal(
self, expr, func, args,
);
}
if self.settings.enabled.contains(&RuleCode::C417) {
if let Some(diagnostic) = flake8_comprehensions::rules::unnecessary_map(
func,
args,
Range::from_located(expr),
) {
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_map(self, expr, func, args);
}
// flake8-boolean-trap
@@ -2420,6 +2326,11 @@ where
args, keywords,
));
}
// flake8-simplify
if self.settings.enabled.contains(&RuleCode::SIM115) {
flake8_simplify::rules::open_file_with_context_handler(self, func);
}
}
ExprKind::Dict { keys, values } => {
if self.settings.enabled.contains(&RuleCode::F601)
@@ -2759,18 +2670,9 @@ where
}
ExprKind::ListComp { elt, generators } | ExprKind::SetComp { elt, generators } => {
if self.settings.enabled.contains(&RuleCode::C416) {
if let Some(diagnostic) =
flake8_comprehensions::rules::unnecessary_comprehension(
expr,
elt,
generators,
self.locator,
self.patch(&RuleCode::C416),
Range::from_located(expr),
)
{
self.diagnostics.push(diagnostic);
};
flake8_comprehensions::rules::unnecessary_comprehension(
self, expr, elt, generators,
);
}
if self.settings.enabled.contains(&RuleCode::B023) {
flake8_bugbear::rules::function_uses_loop_variable(self, &Node::Expr(expr));

View File

@@ -7,33 +7,12 @@ use rustpython_parser::ast::Suite;
use crate::ast::visitor::Visitor;
use crate::directives::IsortDirectives;
use crate::isort;
use crate::isort::track::ImportTracker;
use crate::registry::Diagnostic;
use crate::isort::track::{Block, ImportTracker};
use crate::registry::{Diagnostic, RuleCode};
use crate::settings::{flags, Settings};
use crate::source_code_locator::SourceCodeLocator;
use crate::source_code_style::SourceCodeStyleDetector;
fn check_import_blocks(
tracker: ImportTracker,
locator: &SourceCodeLocator,
settings: &Settings,
stylist: &SourceCodeStyleDetector,
autofix: flags::Autofix,
package: Option<&Path>,
) -> Vec<Diagnostic> {
let mut diagnostics = vec![];
for block in tracker.into_iter() {
if !block.imports.is_empty() {
if let Some(diagnostic) =
isort::rules::check_imports(&block, locator, settings, stylist, autofix, package)
{
diagnostics.push(diagnostic);
}
}
}
diagnostics
}
#[allow(clippy::too_many_arguments)]
pub fn check_imports(
python_ast: &Suite,
@@ -45,9 +24,33 @@ pub fn check_imports(
path: &Path,
package: Option<&Path>,
) -> Vec<Diagnostic> {
let mut tracker = ImportTracker::new(locator, directives, path);
for stmt in python_ast {
tracker.visit_stmt(stmt);
// Extract all imports from the AST.
let tracker = {
let mut tracker = ImportTracker::new(locator, directives, path);
for stmt in python_ast {
tracker.visit_stmt(stmt);
}
tracker
};
let blocks: Vec<&Block> = tracker.iter().collect();
// Enforce import rules.
let mut diagnostics = vec![];
if settings.enabled.contains(&RuleCode::I001) {
for block in &blocks {
if !block.imports.is_empty() {
if let Some(diagnostic) = isort::rules::organize_imports(
block, locator, settings, stylist, autofix, package,
) {
diagnostics.push(diagnostic);
}
}
}
}
check_import_blocks(tracker, locator, settings, stylist, autofix, package)
if settings.enabled.contains(&RuleCode::I002) {
diagnostics.extend(isort::rules::add_required_imports(
&blocks, python_ast, locator, settings, autofix,
));
}
diagnostics
}

View File

@@ -1,6 +1,6 @@
//! Lint rules based on checking raw physical lines.
use crate::pycodestyle::rules::{line_too_long, no_newline_at_end_of_file};
use crate::pycodestyle::rules::{doc_line_too_long, line_too_long, no_newline_at_end_of_file};
use crate::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore};
use crate::pyupgrade::rules::unnecessary_coding_comment;
use crate::registry::{Diagnostic, RuleCode};
@@ -9,18 +9,21 @@ use crate::settings::{flags, Settings};
pub fn check_lines(
contents: &str,
commented_lines: &[usize],
doc_lines: &[usize],
settings: &Settings,
autofix: flags::Autofix,
) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![];
let enforce_unnecessary_coding_comment = settings.enabled.contains(&RuleCode::UP009);
let enforce_blanket_noqa = settings.enabled.contains(&RuleCode::PGH004);
let enforce_blanket_type_ignore = settings.enabled.contains(&RuleCode::PGH003);
let enforce_doc_line_too_long = settings.enabled.contains(&RuleCode::W505);
let enforce_line_too_long = settings.enabled.contains(&RuleCode::E501);
let enforce_no_newline_at_end_of_file = settings.enabled.contains(&RuleCode::W292);
let enforce_blanket_type_ignore = settings.enabled.contains(&RuleCode::PGH003);
let enforce_blanket_noqa = settings.enabled.contains(&RuleCode::PGH004);
let enforce_unnecessary_coding_comment = settings.enabled.contains(&RuleCode::UP009);
let mut commented_lines_iter = commented_lines.iter().peekable();
let mut doc_lines_iter = doc_lines.iter().peekable();
for (index, line) in contents.lines().enumerate() {
while commented_lines_iter
.next_if(|lineno| &(index + 1) == *lineno)
@@ -40,18 +43,25 @@ pub fn check_lines(
}
if enforce_blanket_type_ignore {
if commented_lines.contains(&(index + 1)) {
if let Some(diagnostic) = blanket_type_ignore(index, line) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = blanket_type_ignore(index, line) {
diagnostics.push(diagnostic);
}
}
if enforce_blanket_noqa {
if commented_lines.contains(&(index + 1)) {
if let Some(diagnostic) = blanket_noqa(index, line) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = blanket_noqa(index, line) {
diagnostics.push(diagnostic);
}
}
}
while doc_lines_iter
.next_if(|lineno| &(index + 1) == *lineno)
.is_some()
{
if enforce_doc_line_too_long {
if let Some(diagnostic) = doc_line_too_long(index, line, settings) {
diagnostics.push(diagnostic);
}
}
}
@@ -90,6 +100,7 @@ mod tests {
check_lines(
line,
&[],
&[],
&Settings {
line_length,
..Settings::for_rule(RuleCode::E501)

View File

@@ -61,33 +61,33 @@ pub struct Cli {
pub isolated: bool,
/// Comma-separated list of rule codes to enable (or ALL, to enable all
/// rules).
#[arg(long, value_delimiter = ',')]
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub select: Option<Vec<RuleCodePrefix>>,
/// Like --select, but adds additional rule codes on top of the selected
/// ones.
#[arg(long, value_delimiter = ',')]
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub extend_select: Option<Vec<RuleCodePrefix>>,
/// Comma-separated list of rule codes to disable.
#[arg(long, value_delimiter = ',')]
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub ignore: Option<Vec<RuleCodePrefix>>,
/// Like --ignore, but adds additional rule codes on top of the ignored
/// ones.
#[arg(long, value_delimiter = ',')]
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub extend_ignore: Option<Vec<RuleCodePrefix>>,
/// List of paths, used to omit files and/or directories from analysis.
#[arg(long, value_delimiter = ',')]
#[arg(long, value_delimiter = ',', value_name = "FILE_PATTERN")]
pub exclude: Option<Vec<FilePattern>>,
/// Like --exclude, but adds additional files and directories on top of
/// those already excluded.
#[arg(long, value_delimiter = ',')]
#[arg(long, value_delimiter = ',', value_name = "FILE_PATTERN")]
pub extend_exclude: Option<Vec<FilePattern>>,
/// List of rule codes to treat as eligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',')]
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub fixable: Option<Vec<RuleCodePrefix>>,
/// List of rule codes to treat as ineligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',')]
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub unfixable: Option<Vec<RuleCodePrefix>>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]

View File

@@ -1,5 +1,7 @@
use anyhow::{bail, Result};
use libcst_native::{Expr, Import, ImportFrom, Module, SmallStatement, Statement};
use libcst_native::{
Call, Expr, Expression, Import, ImportFrom, Module, SmallStatement, Statement,
};
pub fn match_module(module_text: &str) -> Result<Module> {
match libcst_native::parse_module(module_text, None) {
@@ -8,6 +10,13 @@ pub fn match_module(module_text: &str) -> Result<Module> {
}
}
pub fn match_expression(expression_text: &str) -> Result<Expression> {
match libcst_native::parse_expression(expression_text) {
Ok(expression) => Ok(expression),
Err(_) => bail!("Failed to extract CST from source"),
}
}
pub fn match_expr<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Expr<'b>> {
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
if let Some(SmallStatement::Expr(expr)) = expr.body.first_mut() {
@@ -43,3 +52,11 @@ pub fn match_import_from<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut I
bail!("Expected Statement::Simple")
}
}
pub fn match_call<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut Call<'b>> {
if let Expression::Call(call) = expression {
Ok(call)
} else {
bail!("Expected SmallStatement::Expr")
}
}

View File

@@ -33,6 +33,7 @@ impl Flags {
pub struct IsortDirectives {
pub exclusions: IntSet<usize>,
pub splits: Vec<usize>,
pub skip_file: bool,
}
pub struct Directives {
@@ -89,17 +90,11 @@ pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap<usize, usize> {
pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
let mut exclusions: IntSet<usize> = IntSet::default();
let mut splits: Vec<usize> = Vec::default();
let mut skip_file: bool = false;
let mut off: Option<Location> = None;
let mut last: Option<Location> = None;
for &(start, ref tok, end) in lxr.iter().flatten() {
last = Some(end);
// No need to keep processing, but we do need to determine the last token.
if skip_file {
continue;
}
let Tok::Comment(comment_text) = tok else {
continue;
};
@@ -111,7 +106,10 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
if comment_text == "# isort: split" {
splits.push(start.row());
} else if comment_text == "# isort: skip_file" || comment_text == "# isort:skip_file" {
skip_file = true;
return IsortDirectives {
skip_file: true,
..IsortDirectives::default()
};
} else if off.is_some() {
if comment_text == "# isort: on" {
if let Some(start) = off {
@@ -130,14 +128,7 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
}
}
if skip_file {
// Enforce `isort: skip_file`.
if let Some(end) = last {
for row in 1..=end.row() {
exclusions.insert(row);
}
}
} else if let Some(start) = off {
if let Some(start) = off {
// Enforce unterminated `isort: off`.
if let Some(end) = last {
for row in start.row() + 1..=end.row() {
@@ -145,7 +136,11 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
}
}
}
IsortDirectives { exclusions, splits }
IsortDirectives {
exclusions,
splits,
..IsortDirectives::default()
}
}
#[cfg(test)]
@@ -283,10 +278,7 @@ x = 1
y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
assert_eq!(
extract_isort_directives(&lxr).exclusions,
IntSet::from_iter([1, 2, 3, 4])
);
assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default());
let contents = "# isort: off
x = 1
@@ -295,10 +287,7 @@ y = 2
# isort: skip_file
z = x + 1";
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
assert_eq!(
extract_isort_directives(&lxr).exclusions,
IntSet::from_iter([1, 2, 3, 4, 5, 6])
);
assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default());
}
#[test]

58
src/doc_lines.rs Normal file
View File

@@ -0,0 +1,58 @@
//! Doc line extraction. In this context, a doc line is a line consisting of a
//! standalone comment or a constant string statement.
use rustpython_ast::{Constant, ExprKind, Stmt, StmtKind, Suite};
use rustpython_parser::lexer::{LexResult, Tok};
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
/// Extract doc lines (standalone comments) from a token sequence.
pub fn doc_lines_from_tokens(lxr: &[LexResult]) -> Vec<usize> {
let mut doc_lines: Vec<usize> = Vec::default();
let mut prev: Option<usize> = None;
for (start, tok, end) in lxr.iter().flatten() {
if matches!(tok, Tok::Indent | Tok::Dedent | Tok::Newline) {
continue;
}
if matches!(tok, Tok::Comment(..)) {
if let Some(prev) = prev {
if start.row() > prev {
doc_lines.push(start.row());
}
} else {
doc_lines.push(start.row());
}
}
prev = Some(end.row());
}
doc_lines
}
#[derive(Default)]
struct StringLinesVisitor {
string_lines: Vec<usize>,
}
impl Visitor<'_> for StringLinesVisitor {
fn visit_stmt(&mut self, stmt: &Stmt) {
if let StmtKind::Expr { value } = &stmt.node {
if let ExprKind::Constant {
value: Constant::Str(..),
..
} = &value.node
{
self.string_lines
.extend(value.location.row()..=value.end_location.unwrap().row());
}
}
visitor::walk_stmt(self, stmt);
}
}
/// Extract doc lines (standalone strings) from an AST.
pub fn doc_lines_from_ast(python_ast: &Suite) -> Vec<usize> {
let mut visitor = StringLinesVisitor::default();
visitor.visit_body(python_ast);
visitor.string_lines
}

View File

@@ -319,7 +319,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
helpers::identifier_range(stmt, checker.locator),
));
}
} else if visibility::is_init(stmt) {
} else if visibility::is_init(cast::name(stmt)) {
// Allow omission of return annotation in `__init__` functions, as long as at
// least one argument is typed.
if checker.settings.enabled.contains(&RuleCode::ANN204) {
@@ -341,7 +341,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
checker.diagnostics.push(diagnostic);
}
}
} else if visibility::is_magic(stmt) {
} else if visibility::is_magic(cast::name(stmt)) {
if checker.settings.enabled.contains(&RuleCode::ANN204) {
checker.diagnostics.push(Diagnostic::new(
violations::MissingReturnTypeSpecialMethod(name.to_string()),

View File

@@ -25,6 +25,8 @@ mod tests {
#[test_case(RuleCode::S324, Path::new("S324.py"); "S324")]
#[test_case(RuleCode::S501, Path::new("S501.py"); "S501")]
#[test_case(RuleCode::S506, Path::new("S506.py"); "S506")]
#[test_case(RuleCode::S508, Path::new("S508.py"); "S508")]
#[test_case(RuleCode::S509, Path::new("S509.py"); "S509")]
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -11,6 +11,8 @@ pub use hardcoded_tmp_directory::hardcoded_tmp_directory;
pub use hashlib_insecure_hash_functions::hashlib_insecure_hash_functions;
pub use request_with_no_cert_validation::request_with_no_cert_validation;
pub use request_without_timeout::request_without_timeout;
pub use snmp_insecure_version::snmp_insecure_version;
pub use snmp_weak_cryptography::snmp_weak_cryptography;
pub use unsafe_yaml_load::unsafe_yaml_load;
mod assert_used;
@@ -24,4 +26,6 @@ mod hardcoded_tmp_directory;
mod hashlib_insecure_hash_functions;
mod request_with_no_cert_validation;
mod request_without_timeout;
mod snmp_insecure_version;
mod snmp_weak_cryptography;
mod unsafe_yaml_load;

View File

@@ -0,0 +1,40 @@
use num_traits::{One, Zero};
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Expr, ExprKind, Keyword};
use rustpython_parser::ast::Constant;
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path, SimpleCallArgs};
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::violations;
/// S508
pub fn snmp_insecure_version(
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
from_imports: &FxHashMap<&str, FxHashSet<&str>>,
import_aliases: &FxHashMap<&str, &str>,
) -> Option<Diagnostic> {
let call_path = dealias_call_path(collect_call_paths(func), import_aliases);
if match_call_path(&call_path, "pysnmp.hlapi", "CommunityData", from_imports) {
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(mp_model_arg) = call_args.get_argument("mpModel", None) {
if let ExprKind::Constant {
value: Constant::Int(value),
..
} = &mp_model_arg.node
{
if value.is_zero() || value.is_one() {
return Some(Diagnostic::new(
violations::SnmpInsecureVersion,
Range::from_located(mp_model_arg),
));
}
}
}
}
None
}

View File

@@ -0,0 +1,30 @@
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Expr, Keyword};
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path, SimpleCallArgs};
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::violations;
/// S509
pub fn snmp_weak_cryptography(
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
from_imports: &FxHashMap<&str, FxHashSet<&str>>,
import_aliases: &FxHashMap<&str, &str>,
) -> Option<Diagnostic> {
let call_path = dealias_call_path(collect_call_paths(func), import_aliases);
if match_call_path(&call_path, "pysnmp.hlapi", "UsmUserData", from_imports) {
let call_args = SimpleCallArgs::new(args, keywords);
if call_args.len() < 3 {
return Some(Diagnostic::new(
violations::SnmpWeakCryptography,
Range::from_located(func),
));
}
}
None
}

View File

@@ -0,0 +1,25 @@
---
source: src/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
SnmpInsecureVersion: ~
location:
row: 3
column: 32
end_location:
row: 3
column: 33
fix: ~
parent: ~
- kind:
SnmpInsecureVersion: ~
location:
row: 4
column: 32
end_location:
row: 4
column: 33
fix: ~
parent: ~

View File

@@ -0,0 +1,25 @@
---
source: src/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
SnmpWeakCryptography: ~
location:
row: 4
column: 11
end_location:
row: 4
column: 22
fix: ~
parent: ~
- kind:
SnmpWeakCryptography: ~
location:
row: 5
column: 15
end_location:
row: 5
column: 26
fix: ~
parent: ~

View File

@@ -12,7 +12,9 @@ use crate::violations;
#[derive(Default)]
struct LoadedNamesVisitor<'a> {
// Tuple of: name, defining expression, and defining range.
names: Vec<(&'a str, &'a Expr, Range)>,
loaded: Vec<(&'a str, &'a Expr, Range)>,
// Tuple of: name, defining expression, and defining range.
stored: Vec<(&'a str, &'a Expr, Range)>,
}
/// `Visitor` to collect all used identifiers in a statement.
@@ -22,12 +24,11 @@ where
{
fn visit_expr(&mut self, expr: &'b Expr) {
match &expr.node {
ExprKind::JoinedStr { .. } => {
visitor::walk_expr(self, expr);
}
ExprKind::Name { id, ctx } if matches!(ctx, ExprContext::Load) => {
self.names.push((id, expr, Range::from_located(expr)));
}
ExprKind::Name { id, ctx } => match ctx {
ExprContext::Load => self.loaded.push((id, expr, Range::from_located(expr))),
ExprContext::Store => self.stored.push((id, expr, Range::from_located(expr))),
ExprContext::Del => {}
},
_ => visitor::walk_expr(self, expr),
}
}
@@ -36,6 +37,7 @@ where
#[derive(Default)]
struct SuspiciousVariablesVisitor<'a> {
names: Vec<(&'a str, &'a Expr, Range)>,
safe_functions: Vec<&'a Expr>,
}
/// `Visitor` to collect all suspicious variables (those referenced in
@@ -50,45 +52,90 @@ where
| StmtKind::AsyncFunctionDef { args, body, .. } => {
// Collect all loaded variable names.
let mut visitor = LoadedNamesVisitor::default();
for stmt in body {
visitor.visit_stmt(stmt);
}
visitor.visit_body(body);
// Collect all argument names.
let arg_names = collect_arg_names(args);
let mut arg_names = collect_arg_names(args);
arg_names.extend(visitor.stored.iter().map(|(id, ..)| id));
// Treat any non-arguments as "suspicious".
self.names.extend(
visitor
.names
.into_iter()
.loaded
.iter()
.filter(|(id, ..)| !arg_names.contains(id)),
);
}
_ => visitor::walk_stmt(self, stmt),
StmtKind::Return { value: Some(value) } => {
// Mark `return lambda: x` as safe.
if matches!(value.node, ExprKind::Lambda { .. }) {
self.safe_functions.push(value);
}
}
_ => {}
}
visitor::walk_stmt(self, stmt);
}
fn visit_expr(&mut self, expr: &'b Expr) {
match &expr.node {
ExprKind::Lambda { args, body } => {
// Collect all loaded variable names.
let mut visitor = LoadedNamesVisitor::default();
visitor.visit_expr(body);
// Collect all argument names.
let arg_names = collect_arg_names(args);
// Treat any non-arguments as "suspicious".
self.names.extend(
visitor
.names
.into_iter()
.filter(|(id, ..)| !arg_names.contains(id)),
);
ExprKind::Call {
func,
args,
keywords,
} => {
if let ExprKind::Name { id, .. } = &func.node {
if id == "filter" || id == "reduce" || id == "map" {
for arg in args {
if matches!(arg.node, ExprKind::Lambda { .. }) {
self.safe_functions.push(arg);
}
}
}
}
if let ExprKind::Attribute { value, attr, .. } = &func.node {
if attr == "reduce" {
if let ExprKind::Name { id, .. } = &value.node {
if id == "functools" {
for arg in args {
if matches!(arg.node, ExprKind::Lambda { .. }) {
self.safe_functions.push(arg);
}
}
}
}
}
}
for keyword in keywords {
if keyword.node.arg.as_ref().map_or(false, |arg| arg == "key")
&& matches!(keyword.node.value.node, ExprKind::Lambda { .. })
{
self.safe_functions.push(&keyword.node.value);
}
}
}
_ => visitor::walk_expr(self, expr),
ExprKind::Lambda { args, body } => {
if !self.safe_functions.contains(&expr) {
// Collect all loaded variable names.
let mut visitor = LoadedNamesVisitor::default();
visitor.visit_expr(body);
// Collect all argument names.
let mut arg_names = collect_arg_names(args);
arg_names.extend(visitor.stored.iter().map(|(id, ..)| id));
// Treat any non-arguments as "suspicious".
self.names.extend(
visitor
.loaded
.iter()
.filter(|(id, ..)| !arg_names.contains(id)),
);
}
}
_ => {}
}
visitor::walk_expr(self, expr);
}
}

View File

@@ -1,6 +1,6 @@
---
source: src/flake8_bugbear/mod.rs
expression: checks
expression: diagnostics
---
- kind:
FunctionUsesLoopVariable: x
@@ -172,4 +172,74 @@ expression: checks
column: 16
fix: ~
parent: ~
- kind:
FunctionUsesLoopVariable: x
location:
row: 117
column: 23
end_location:
row: 117
column: 24
fix: ~
parent: ~
- kind:
FunctionUsesLoopVariable: x
location:
row: 118
column: 26
end_location:
row: 118
column: 27
fix: ~
parent: ~
- kind:
FunctionUsesLoopVariable: x
location:
row: 119
column: 36
end_location:
row: 119
column: 37
fix: ~
parent: ~
- kind:
FunctionUsesLoopVariable: x
location:
row: 120
column: 37
end_location:
row: 120
column: 38
fix: ~
parent: ~
- kind:
FunctionUsesLoopVariable: x
location:
row: 121
column: 36
end_location:
row: 121
column: 37
fix: ~
parent: ~
- kind:
FunctionUsesLoopVariable: name
location:
row: 171
column: 28
end_location:
row: 171
column: 32
fix: ~
parent: ~
- kind:
FunctionUsesLoopVariable: i
location:
row: 174
column: 28
end_location:
row: 174
column: 29
fix: ~
parent: ~

View File

@@ -1,13 +1,11 @@
use log::error;
use num_bigint::BigInt;
use rustpython_ast::{
Comprehension, Constant, Expr, ExprKind, Keyword, KeywordData, Located, Unaryop,
};
use rustpython_ast::{Comprehension, Constant, Expr, ExprKind, Keyword, Unaryop};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::flake8_comprehensions::fixes;
use crate::registry::Diagnostic;
use crate::source_code_locator::SourceCodeLocator;
use crate::registry::{Diagnostic, RuleCode};
use crate::violations;
fn function_name(func: &Expr) -> Option<&str> {
@@ -41,237 +39,266 @@ fn first_argument_with_matching_function<'a>(
func: &Expr,
args: &'a [Expr],
) -> Option<&'a ExprKind> {
if function_name(func)? != name {
return None;
if function_name(func)? == name {
Some(&args.first()?.node)
} else {
None
}
Some(&args.first()?.node)
}
/// C400 (`list(generator)`)
pub fn unnecessary_generator_list(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
let argument = exactly_one_argument_with_matching_function("list", func, args, keywords)?;
) {
let Some(argument) = exactly_one_argument_with_matching_function("list", func, args, keywords) else {
return;
};
if !checker.is_builtin("list") {
return;
}
if let ExprKind::GeneratorExp { .. } = argument {
let mut diagnostic = Diagnostic::new(violations::UnnecessaryGeneratorList, location);
if fix {
match fixes::fix_unnecessary_generator_list(locator, expr) {
let mut diagnostic = Diagnostic::new(
violations::UnnecessaryGeneratorList,
Range::from_located(expr),
);
if checker.patch(&RuleCode::C400) {
match fixes::fix_unnecessary_generator_list(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
return Some(diagnostic);
checker.diagnostics.push(diagnostic);
}
None
}
/// C401 (`set(generator)`)
pub fn unnecessary_generator_set(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
let argument = exactly_one_argument_with_matching_function("set", func, args, keywords)?;
) {
let Some(argument) = exactly_one_argument_with_matching_function("set", func, args, keywords) else {
return;
};
if !checker.is_builtin("set") {
return;
}
if let ExprKind::GeneratorExp { .. } = argument {
let mut diagnostic = Diagnostic::new(violations::UnnecessaryGeneratorSet, location);
if fix {
match fixes::fix_unnecessary_generator_set(locator, expr) {
let mut diagnostic = Diagnostic::new(
violations::UnnecessaryGeneratorSet,
Range::from_located(expr),
);
if checker.patch(&RuleCode::C401) {
match fixes::fix_unnecessary_generator_set(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
return Some(diagnostic);
checker.diagnostics.push(diagnostic);
}
None
}
/// C402 (`dict((x, y) for x, y in iterable)`)
pub fn unnecessary_generator_dict(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
let argument = exactly_one_argument_with_matching_function("dict", func, args, keywords)?;
) {
let Some(argument) = exactly_one_argument_with_matching_function("dict", func, args, keywords) else {
return;
};
if let ExprKind::GeneratorExp { elt, .. } = argument {
match &elt.node {
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
let mut diagnostic =
Diagnostic::new(violations::UnnecessaryGeneratorDict, location);
if fix {
match fixes::fix_unnecessary_generator_dict(locator, expr) {
let mut diagnostic = Diagnostic::new(
violations::UnnecessaryGeneratorDict,
Range::from_located(expr),
);
if checker.patch(&RuleCode::C402) {
match fixes::fix_unnecessary_generator_dict(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
return Some(diagnostic);
checker.diagnostics.push(diagnostic);
}
_ => {}
}
}
None
}
/// C403 (`set([...])`)
pub fn unnecessary_list_comprehension_set(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
let argument = exactly_one_argument_with_matching_function("set", func, args, keywords)?;
) {
let Some(argument) = exactly_one_argument_with_matching_function("set", func, args, keywords) else {
return;
};
if !checker.is_builtin("set") {
return;
}
if let ExprKind::ListComp { .. } = &argument {
let mut diagnostic = Diagnostic::new(violations::UnnecessaryListComprehensionSet, location);
if fix {
match fixes::fix_unnecessary_list_comprehension_set(locator, expr) {
let mut diagnostic = Diagnostic::new(
violations::UnnecessaryListComprehensionSet,
Range::from_located(expr),
);
if checker.patch(&RuleCode::C403) {
match fixes::fix_unnecessary_list_comprehension_set(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
return Some(diagnostic);
checker.diagnostics.push(diagnostic);
}
None
}
/// C404 (`dict([...])`)
pub fn unnecessary_list_comprehension_dict(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
let argument = exactly_one_argument_with_matching_function("dict", func, args, keywords)?;
) {
let Some(argument) = exactly_one_argument_with_matching_function("dict", func, args, keywords) else {
return;
};
if !checker.is_builtin("dict") {
return;
}
let ExprKind::ListComp { elt, .. } = &argument else {
return None;
return;
};
let ExprKind::Tuple { elts, .. } = &elt.node else {
return None;
return;
};
if elts.len() != 2 {
return None;
return;
}
let mut diagnostic = Diagnostic::new(violations::UnnecessaryListComprehensionDict, location);
if fix {
match fixes::fix_unnecessary_list_comprehension_dict(locator, expr) {
let mut diagnostic = Diagnostic::new(
violations::UnnecessaryListComprehensionDict,
Range::from_located(expr),
);
if checker.patch(&RuleCode::C404) {
match fixes::fix_unnecessary_list_comprehension_dict(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
Some(diagnostic)
checker.diagnostics.push(diagnostic);
}
/// C405 (`set([1, 2])`)
pub fn unnecessary_literal_set(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
let argument = exactly_one_argument_with_matching_function("set", func, args, keywords)?;
) {
let Some(argument) = exactly_one_argument_with_matching_function("set", func, args, keywords) else {
return;
};
if !checker.is_builtin("set") {
return;
}
let kind = match argument {
ExprKind::List { .. } => "list",
ExprKind::Tuple { .. } => "tuple",
_ => return None,
_ => return,
};
let mut diagnostic = Diagnostic::new(
violations::UnnecessaryLiteralSet(kind.to_string()),
location,
Range::from_located(expr),
);
if fix {
match fixes::fix_unnecessary_literal_set(locator, expr) {
if checker.patch(&RuleCode::C405) {
match fixes::fix_unnecessary_literal_set(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
Some(diagnostic)
checker.diagnostics.push(diagnostic);
}
/// C406 (`dict([(1, 2)])`)
pub fn unnecessary_literal_dict(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
let argument = exactly_one_argument_with_matching_function("dict", func, args, keywords)?;
) {
let Some(argument) = exactly_one_argument_with_matching_function("dict", func, args, keywords) else {
return;
};
if !checker.is_builtin("dict") {
return;
}
let (kind, elts) = match argument {
ExprKind::Tuple { elts, .. } => ("tuple", elts),
ExprKind::List { elts, .. } => ("list", elts),
_ => return None,
_ => return,
};
// Accept `dict((1, 2), ...))` `dict([(1, 2), ...])`.
if !elts
.iter()
.all(|elt| matches!(&elt.node, ExprKind::Tuple { elts, .. } if elts.len() == 2))
{
return None;
return;
}
let mut diagnostic = Diagnostic::new(
violations::UnnecessaryLiteralDict(kind.to_string()),
location,
Range::from_located(expr),
);
if fix {
match fixes::fix_unnecessary_literal_dict(locator, expr) {
if checker.patch(&RuleCode::C406) {
match fixes::fix_unnecessary_literal_dict(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
Some(diagnostic)
checker.diagnostics.push(diagnostic);
}
/// C408
pub fn unnecessary_collection_call(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Located<KeywordData>],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
keywords: &[Keyword],
) {
if !args.is_empty() {
return None;
return;
}
let id = function_name(func)?;
let Some(id) = function_name(func) else {
return;
};
match id {
"dict" if keywords.is_empty() || keywords.iter().all(|kw| kw.node.arg.is_some()) => {
// `dict()` or `dict(a=1)` (as opposed to `dict(**a)`)
@@ -279,296 +306,377 @@ pub fn unnecessary_collection_call(
"list" | "tuple" => {
// `list()` or `tuple()`
}
_ => return None,
_ => return,
};
if !checker.is_builtin(id) {
return;
}
let mut diagnostic = Diagnostic::new(
violations::UnnecessaryCollectionCall(id.to_string()),
location,
Range::from_located(expr),
);
if fix {
match fixes::fix_unnecessary_collection_call(locator, expr) {
if checker.patch(&RuleCode::C408) {
match fixes::fix_unnecessary_collection_call(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
Some(diagnostic)
checker.diagnostics.push(diagnostic);
}
/// C409
pub fn unnecessary_literal_within_tuple_call(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
let argument = first_argument_with_matching_function("tuple", func, args)?;
) {
let Some(argument) = first_argument_with_matching_function("tuple", func, args) else {
return;
};
if !checker.is_builtin("tuple") {
return;
}
let argument_kind = match argument {
ExprKind::Tuple { .. } => "tuple",
ExprKind::List { .. } => "list",
_ => return None,
_ => return,
};
let mut diagnostic = Diagnostic::new(
violations::UnnecessaryLiteralWithinTupleCall(argument_kind.to_string()),
location,
Range::from_located(expr),
);
if fix {
match fixes::fix_unnecessary_literal_within_tuple_call(locator, expr) {
if checker.patch(&RuleCode::C409) {
match fixes::fix_unnecessary_literal_within_tuple_call(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
Some(diagnostic)
checker.diagnostics.push(diagnostic);
}
/// C410
pub fn unnecessary_literal_within_list_call(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
let argument = first_argument_with_matching_function("list", func, args)?;
) {
let Some(argument) = first_argument_with_matching_function("list", func, args) else {
return;
};
if !checker.is_builtin("list") {
return;
}
let argument_kind = match argument {
ExprKind::Tuple { .. } => "tuple",
ExprKind::List { .. } => "list",
_ => return None,
_ => return,
};
let mut diagnostic = Diagnostic::new(
violations::UnnecessaryLiteralWithinListCall(argument_kind.to_string()),
location,
Range::from_located(expr),
);
if fix {
match fixes::fix_unnecessary_literal_within_list_call(locator, expr) {
if checker.patch(&RuleCode::C410) {
match fixes::fix_unnecessary_literal_within_list_call(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
Some(diagnostic)
checker.diagnostics.push(diagnostic);
}
/// C411
pub fn unnecessary_list_call(
expr: &Expr,
func: &Expr,
args: &[Expr],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
let argument = first_argument_with_matching_function("list", func, args)?;
if !matches!(argument, ExprKind::ListComp { .. }) {
return None;
pub fn unnecessary_list_call(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
let Some(argument) = first_argument_with_matching_function("list", func, args) else {
return;
};
if !checker.is_builtin("list") {
return;
}
let mut diagnostic = Diagnostic::new(violations::UnnecessaryListCall, location);
if fix {
match fixes::fix_unnecessary_list_call(locator, expr) {
if !matches!(argument, ExprKind::ListComp { .. }) {
return;
}
let mut diagnostic =
Diagnostic::new(violations::UnnecessaryListCall, Range::from_located(expr));
if checker.patch(&RuleCode::C411) {
match fixes::fix_unnecessary_list_call(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
Some(diagnostic)
checker.diagnostics.push(diagnostic);
}
/// C413
pub fn unnecessary_call_around_sorted(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
let outer = function_name(func)?;
if !(outer == "list" || outer == "reversed") {
return None;
}
let ExprKind::Call { func, .. } = &args.first()?.node else {
return None;
) {
let Some(outer) = function_name(func) else {
return;
};
if function_name(func)? != "sorted" {
return None;
if !(outer == "list" || outer == "reversed") {
return;
}
let Some(arg) = args.first() else {
return;
};
let ExprKind::Call { func, .. } = &arg.node else {
return;
};
let Some(inner) = function_name(func) else {
return;
};
if inner != "sorted" {
return;
}
if !checker.is_builtin(inner) || !checker.is_builtin(outer) {
return;
}
let mut diagnostic = Diagnostic::new(
violations::UnnecessaryCallAroundSorted(outer.to_string()),
location,
Range::from_located(expr),
);
if fix {
match fixes::fix_unnecessary_call_around_sorted(locator, expr) {
if checker.patch(&RuleCode::C413) {
match fixes::fix_unnecessary_call_around_sorted(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
Some(diagnostic)
checker.diagnostics.push(diagnostic);
}
/// C414
pub fn unnecessary_double_cast_or_process(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
location: Range,
) -> Option<Diagnostic> {
fn new_check(inner: &str, outer: &str, location: Range) -> Diagnostic {
) {
fn diagnostic(inner: &str, outer: &str, location: Range) -> Diagnostic {
Diagnostic::new(
violations::UnnecessaryDoubleCastOrProcess(inner.to_string(), outer.to_string()),
location,
)
}
let outer = function_name(func)?;
if !["list", "tuple", "set", "reversed", "sorted"].contains(&outer) {
return None;
let Some(outer) = function_name(func) else {
return;
};
if !(outer == "list"
|| outer == "tuple"
|| outer == "set"
|| outer == "reversed"
|| outer == "sorted")
{
return;
}
let Some(arg) = args.first() else {
return;
};
let ExprKind::Call { func, .. } = &arg.node else {
return;
};
let Some(inner) = function_name(func) else {
return;
};
if !checker.is_builtin(inner) || !checker.is_builtin(outer) {
return;
}
let ExprKind::Call { func, .. } = &args.first()?.node else {
return None;
};
let inner = function_name(func)?;
// Ex) set(tuple(...))
if (outer == "set" || outer == "sorted")
&& (inner == "list" || inner == "tuple" || inner == "reversed" || inner == "sorted")
{
return Some(new_check(inner, outer, location));
checker
.diagnostics
.push(diagnostic(inner, outer, Range::from_located(expr)));
return;
}
// Ex) list(tuple(...))
if (outer == "list" || outer == "tuple") && (inner == "list" || inner == "tuple") {
return Some(new_check(inner, outer, location));
checker
.diagnostics
.push(diagnostic(inner, outer, Range::from_located(expr)));
return;
}
// Ex) set(set(...))
if outer == "set" && inner == "set" {
return Some(new_check(inner, outer, location));
checker
.diagnostics
.push(diagnostic(inner, outer, Range::from_located(expr)));
}
None
}
/// C415
pub fn unnecessary_subscript_reversal(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
location: Range,
) -> Option<Diagnostic> {
let first_arg = args.first()?;
let id = function_name(func)?;
if !["set", "sorted", "reversed"].contains(&id) {
return None;
) {
let Some(first_arg) = args.first() else {
return;
};
let Some(id) = function_name(func) else {
return;
};
if !(id == "set" || id == "sorted" || id == "reversed") {
return;
}
if !checker.is_builtin(id) {
return;
}
let ExprKind::Subscript { slice, .. } = &first_arg.node else {
return None;
return;
};
let ExprKind::Slice { lower, upper, step } = &slice.node else {
return None;
return;
};
if lower.is_some() || upper.is_some() {
return None;
return;
}
let Some(step) = step.as_ref() else {
return;
};
let ExprKind::UnaryOp {
op: Unaryop::USub,
operand,
} = &step.as_ref()?.node else {
return None;
} = &step.node else {
return;
};
let ExprKind::Constant {
value: Constant::Int(val),
..
} = &operand.node else {
return None;
return;
};
if *val != BigInt::from(1) {
return None;
return;
};
Some(Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
violations::UnnecessarySubscriptReversal(id.to_string()),
location,
))
Range::from_located(expr),
));
}
/// C416
pub fn unnecessary_comprehension(
checker: &mut Checker,
expr: &Expr,
elt: &Expr,
generators: &[Comprehension],
locator: &SourceCodeLocator,
fix: bool,
location: Range,
) -> Option<Diagnostic> {
) {
if generators.len() != 1 {
return None;
return;
}
let generator = &generators[0];
if !(generator.ifs.is_empty() && generator.is_async == 0) {
return None;
return;
}
let elt_id = function_name(elt)?;
let target_id = function_name(&generator.target)?;
let Some(elt_id) = function_name(elt) else {
return;
};
let Some(target_id) = function_name(&generator.target) else {
return;
};
if elt_id != target_id {
return None;
return;
}
let expr_kind = match &expr.node {
let id = match &expr.node {
ExprKind::ListComp { .. } => "list",
ExprKind::SetComp { .. } => "set",
_ => return None,
_ => return,
};
if !checker.is_builtin(id) {
return;
}
let mut diagnostic = Diagnostic::new(
violations::UnnecessaryComprehension(expr_kind.to_string()),
location,
violations::UnnecessaryComprehension(id.to_string()),
Range::from_located(expr),
);
if fix {
match fixes::fix_unnecessary_comprehension(locator, expr) {
if checker.patch(&RuleCode::C416) {
match fixes::fix_unnecessary_comprehension(checker.locator, expr) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
Some(diagnostic)
checker.diagnostics.push(diagnostic);
}
/// C417
pub fn unnecessary_map(func: &Expr, args: &[Expr], location: Range) -> Option<Diagnostic> {
fn new_check(kind: &str, location: Range) -> Diagnostic {
pub fn unnecessary_map(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
fn diagnostic(kind: &str, location: Range) -> Diagnostic {
Diagnostic::new(violations::UnnecessaryMap(kind.to_string()), location)
}
let id = function_name(func)?;
let Some(id) = function_name(func) else {
return;
};
match id {
"map" => {
if !checker.is_builtin(id) {
return;
}
if args.len() == 2 && matches!(&args[0].node, ExprKind::Lambda { .. }) {
return Some(new_check("generator", location));
checker
.diagnostics
.push(diagnostic("generator", Range::from_located(expr)));
}
}
"list" | "set" => {
if let ExprKind::Call { func, args, .. } = &args.first()?.node {
let argument = first_argument_with_matching_function("map", func, args)?;
if let ExprKind::Lambda { .. } = argument {
return Some(new_check(id, location));
if !checker.is_builtin(id) {
return;
}
if let Some(arg) = args.first() {
if let ExprKind::Call { func, args, .. } = &arg.node {
let Some(argument) = first_argument_with_matching_function("map", func, args) else {
return;
};
if let ExprKind::Lambda { .. } = argument {
checker
.diagnostics
.push(diagnostic(id, Range::from_located(expr)));
}
}
}
}
"dict" => {
if !checker.is_builtin(id) {
return;
}
if args.len() == 1 {
if let ExprKind::Call { func, args, .. } = &args[0].node {
let argument = first_argument_with_matching_function("map", func, args)?;
let Some(argument) = first_argument_with_matching_function("map", func, args) else {
return;
};
if let ExprKind::Lambda { body, .. } = &argument {
if matches!(&body.node, ExprKind::Tuple { elts, .. } | ExprKind::List { elts, .. } if elts.len() == 2)
{
return Some(new_check(id, location));
checker
.diagnostics
.push(diagnostic(id, Range::from_located(expr)));
}
}
}
@@ -576,5 +684,4 @@ pub fn unnecessary_map(func: &Expr, args: &[Expr], location: Range) -> Option<Di
}
_ => (),
}
None
}

View File

@@ -1,6 +1,6 @@
---
source: src/flake8_comprehensions/mod.rs
expression: checks
expression: diagnostics
---
- kind:
UnnecessaryGeneratorList: ~

View File

@@ -1,6 +1,6 @@
---
source: src/flake8_comprehensions/mod.rs
expression: checks
expression: diagnostics
---
- kind:
UnnecessaryGeneratorSet: ~

View File

@@ -1,6 +1,6 @@
---
source: src/flake8_comprehensions/mod.rs
expression: checks
expression: diagnostics
---
- kind:
UnnecessaryGeneratorDict: ~

View File

@@ -2,7 +2,7 @@ use log::error;
use rustc_hash::FxHashSet;
use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::ast::types::{Range, RefEquality};
use crate::autofix::helpers::delete_stmt;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
@@ -48,12 +48,14 @@ pub fn no_unnecessary_pass(checker: &mut Checker, body: &[Stmt]) {
}
/// PIE794
pub fn dupe_class_field_definitions(checker: &mut Checker, bases: &[Expr], body: &[Stmt]) {
if bases.is_empty() {
return;
}
let mut seen_targets = FxHashSet::default();
pub fn dupe_class_field_definitions<'a, 'b>(
checker: &mut Checker<'a>,
parent: &'b Stmt,
body: &'b [Stmt],
) where
'b: 'a,
{
let mut seen_targets: FxHashSet<&str> = FxHashSet::default();
for stmt in body {
// Extract the property name from the assignment statement.
let target = match &stmt.node {
@@ -77,17 +79,29 @@ pub fn dupe_class_field_definitions(checker: &mut Checker, bases: &[Expr], body:
_ => continue,
};
if seen_targets.contains(target) {
if !seen_targets.insert(target) {
let mut diagnostic = Diagnostic::new(
violations::DupeClassFieldDefinitions(target.to_string()),
Range::from_located(stmt),
);
if checker.patch(&RuleCode::PIE794) {
diagnostic.amend(Fix::deletion(stmt.location, stmt.end_location.unwrap()));
let deleted: Vec<&Stmt> = checker
.deletions
.iter()
.map(std::convert::Into::into)
.collect();
let locator = checker.locator;
match delete_stmt(stmt, Some(parent), &deleted, locator) {
Ok(fix) => {
checker.deletions.insert(RefEquality(stmt));
diagnostic.amend(fix);
}
Err(err) => {
error!("Failed to remove duplicate class definition: {}", err);
}
}
}
checker.diagnostics.push(diagnostic);
} else {
seen_targets.insert(target);
}
}
}

View File

@@ -1,6 +1,6 @@
---
source: src/flake8_pie/mod.rs
expression: checks
expression: diagnostics
---
- kind:
DupeClassFieldDefinitions: name
@@ -14,10 +14,10 @@ expression: checks
content: ""
location:
row: 4
column: 4
column: 0
end_location:
row: 4
column: 24
row: 5
column: 0
parent: ~
- kind:
DupeClassFieldDefinitions: name
@@ -31,10 +31,10 @@ expression: checks
content: ""
location:
row: 13
column: 4
column: 0
end_location:
row: 13
column: 24
row: 14
column: 0
parent: ~
- kind:
DupeClassFieldDefinitions: bar
@@ -48,9 +48,26 @@ expression: checks
content: ""
location:
row: 23
column: 4
column: 0
end_location:
row: 23
column: 23
row: 24
column: 0
parent: ~
- kind:
DupeClassFieldDefinitions: bar
location:
row: 40
column: 4
end_location:
row: 40
column: 23
fix:
content: ""
location:
row: 40
column: 0
end_location:
row: 41
column: 0
parent: ~

View File

@@ -4,7 +4,7 @@ use super::helpers::{
get_mark_decorators, get_mark_name, is_abstractmethod_decorator, is_pytest_fixture,
is_pytest_yield_fixture, keyword_is_literal,
};
use crate::ast::helpers::{collect_arg_names, collect_call_paths, identifier_range};
use crate::ast::helpers::{collect_arg_names, collect_call_paths};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
@@ -156,33 +156,19 @@ fn check_fixture_returns(checker: &mut Checker, func: &Stmt, func_name: &str, bo
&& visitor.has_return_with_value
&& func_name.starts_with('_')
{
let mut diagnostic = Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
violations::IncorrectFixtureNameUnderscore(func_name.to_string()),
Range::from_located(func),
);
if checker.patch(diagnostic.kind.code()) {
let func_name_range = identifier_range(func, checker.locator);
let num_underscores = func_name.len() - func_name.trim_start_matches('_').len();
diagnostic.amend(Fix::deletion(
func_name_range.location,
func_name_range.location.with_col_offset(num_underscores),
));
}
checker.diagnostics.push(diagnostic);
));
} else if checker.settings.enabled.contains(&RuleCode::PT004)
&& !visitor.has_return_with_value
&& !visitor.has_yield_from
&& !func_name.starts_with('_')
{
let mut diagnostic = Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
violations::MissingFixtureNameUnderscore(func_name.to_string()),
Range::from_located(func),
);
if checker.patch(diagnostic.kind.code()) {
let func_name_range = identifier_range(func, checker.locator);
diagnostic.amend(Fix::insertion("_".to_string(), func_name_range.location));
}
checker.diagnostics.push(diagnostic);
));
}
if checker.settings.enabled.contains(&RuleCode::PT022) {

View File

@@ -10,14 +10,7 @@ expression: diagnostics
end_location:
row: 52
column: 30
fix:
content: _
location:
row: 51
column: 4
end_location:
row: 51
column: 4
fix: ~
parent: ~
- kind:
MissingFixtureNameUnderscore: activate_context
@@ -27,13 +20,6 @@ expression: diagnostics
end_location:
row: 58
column: 13
fix:
content: _
location:
row: 56
column: 4
end_location:
row: 56
column: 4
fix: ~
parent: ~

View File

@@ -10,14 +10,7 @@ expression: diagnostics
end_location:
row: 42
column: 12
fix:
content: ""
location:
row: 41
column: 4
end_location:
row: 41
column: 5
fix: ~
parent: ~
- kind:
IncorrectFixtureNameUnderscore: _activate_context
@@ -27,14 +20,7 @@ expression: diagnostics
end_location:
row: 48
column: 21
fix:
content: ""
location:
row: 46
column: 4
end_location:
row: 46
column: 5
fix: ~
parent: ~
- kind:
IncorrectFixtureNameUnderscore: _activate_context
@@ -44,13 +30,6 @@ expression: diagnostics
end_location:
row: 57
column: 34
fix:
content: ""
location:
row: 52
column: 4
end_location:
row: 52
column: 5
fix: ~
parent: ~

View File

@@ -36,8 +36,8 @@ fn good_multiline_ending(quote: &Quote) -> &str {
fn good_docstring(quote: &Quote) -> &str {
match quote {
Quote::Single => "'''",
Quote::Double => "\"\"\"",
Quote::Single => "'",
Quote::Double => "\"",
}
}

View File

@@ -1,6 +1,6 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
expression: diagnostics
---
- kind:
BadQuotesMultilineString: single
@@ -45,10 +45,10 @@ expression: checks
- kind:
BadQuotesMultilineString: single
location:
row: 21
row: 22
column: 4
end_location:
row: 21
row: 22
column: 27
fix: ~
parent: ~

View File

@@ -1,6 +1,6 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
expression: diagnostics
---
- kind:
BadQuotesDocstring: double
@@ -22,4 +22,14 @@ expression: checks
column: 7
fix: ~
parent: ~
- kind:
BadQuotesDocstring: double
location:
row: 27
column: 4
end_location:
row: 27
column: 27
fix: ~
parent: ~

View File

@@ -1,6 +1,6 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
expression: diagnostics
---
- kind:
BadQuotesDocstring: single
@@ -22,4 +22,14 @@ expression: checks
column: 7
fix: ~
parent: ~
- kind:
BadQuotesDocstring: single
location:
row: 27
column: 4
end_location:
row: 27
column: 27
fix: ~
parent: ~

View File

@@ -1,6 +1,6 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
expression: diagnostics
---
- kind:
BadQuotesMultilineString: double
@@ -45,10 +45,10 @@ expression: checks
- kind:
BadQuotesMultilineString: double
location:
row: 21
row: 22
column: 4
end_location:
row: 21
row: 22
column: 27
fix: ~
parent: ~

View File

@@ -21,19 +21,22 @@ mod tests {
#[test_case(RuleCode::SIM109, Path::new("SIM109.py"); "SIM109")]
#[test_case(RuleCode::SIM110, Path::new("SIM110.py"); "SIM110")]
#[test_case(RuleCode::SIM111, Path::new("SIM111.py"); "SIM111")]
#[test_case(RuleCode::SIM112, Path::new("SIM112.py"); "SIM112")]
#[test_case(RuleCode::SIM115, Path::new("SIM115.py"); "SIM115")]
#[test_case(RuleCode::SIM117, Path::new("SIM117.py"); "SIM117")]
#[test_case(RuleCode::SIM118, Path::new("SIM118.py"); "SIM118")]
#[test_case(RuleCode::SIM201, Path::new("SIM201.py"); "SIM201")]
#[test_case(RuleCode::SIM202, Path::new("SIM202.py"); "SIM202")]
#[test_case(RuleCode::SIM208, Path::new("SIM208.py"); "SIM208")]
#[test_case(RuleCode::SIM210, Path::new("SIM210.py"); "SIM210")]
#[test_case(RuleCode::SIM211, Path::new("SIM211.py"); "SIM211")]
#[test_case(RuleCode::SIM212, Path::new("SIM212.py"); "SIM212")]
#[test_case(RuleCode::SIM118, Path::new("SIM118.py"); "SIM118")]
#[test_case(RuleCode::SIM220, Path::new("SIM220.py"); "SIM220")]
#[test_case(RuleCode::SIM221, Path::new("SIM221.py"); "SIM221")]
#[test_case(RuleCode::SIM222, Path::new("SIM222.py"); "SIM222")]
#[test_case(RuleCode::SIM223, Path::new("SIM223.py"); "SIM223")]
#[test_case(RuleCode::SIM300, Path::new("SIM300.py"); "SIM300")]
#[test_case(RuleCode::SIM401, Path::new("SIM401.py"); "SIM401")]
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -0,0 +1,106 @@
use rustpython_ast::{Constant, Expr, ExprKind};
use crate::ast::helpers::{create_expr, match_module_member, unparse_expr};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::registry::{Diagnostic, RuleCode};
use crate::violations;
/// SIM112
pub fn use_capital_environment_variables(checker: &mut Checker, expr: &Expr) {
// check `os.environ['foo']`
if let ExprKind::Subscript { .. } = &expr.node {
check_os_environ_subscript(checker, expr);
return;
}
// check `os.environ.get('foo')` and `os.getenv('foo')``
let is_os_environ_get = match_module_member(
expr,
"os.environ",
"get",
&checker.from_imports,
&checker.import_aliases,
);
let is_os_getenv = match_module_member(
expr,
"os",
"getenv",
&checker.from_imports,
&checker.import_aliases,
);
if !(is_os_environ_get || is_os_getenv) {
return;
}
let ExprKind::Call { args, .. } = &expr.node else {
return;
};
let Some(arg) = args.get(0) else {
return;
};
let ExprKind::Constant { value: Constant::Str(env_var), kind } = &arg.node else {
return;
};
let capital_env_var = env_var.to_ascii_uppercase();
if &capital_env_var == env_var {
return;
}
let mut diagnostic = Diagnostic::new(
violations::UseCapitalEnvironmentVariables(capital_env_var.clone(), env_var.clone()),
Range::from_located(arg),
);
if checker.patch(&RuleCode::SIM112) {
let new_env_var = create_expr(ExprKind::Constant {
value: capital_env_var.into(),
kind: kind.clone(),
});
diagnostic.amend(Fix::replacement(
unparse_expr(&new_env_var, checker.style),
arg.location,
arg.end_location.unwrap(),
));
}
checker.diagnostics.push(diagnostic);
}
fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) {
let ExprKind::Subscript { value, slice, .. } = &expr.node else {
return;
};
let ExprKind::Attribute { value: attr_value, attr, .. } = &value.node else {
return;
};
let ExprKind::Name { id, .. } = &attr_value.node else {
return;
};
if id != "os" || attr != "environ" {
return;
}
let ExprKind::Constant { value: Constant::Str(env_var), kind } = &slice.node else {
return;
};
let capital_env_var = env_var.to_ascii_uppercase();
if &capital_env_var == env_var {
return;
}
let mut diagnostic = Diagnostic::new(
violations::UseCapitalEnvironmentVariables(capital_env_var.clone(), env_var.clone()),
Range::from_located(slice),
);
if checker.patch(&RuleCode::SIM112) {
let new_env_var = create_expr(ExprKind::Constant {
value: capital_env_var.into(),
kind: kind.clone(),
});
diagnostic.amend(Fix::replacement(
unparse_expr(&new_env_var, checker.style),
slice.location,
slice.end_location.unwrap(),
));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,7 +1,8 @@
use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use rustpython_ast::{Cmpop, Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind};
use crate::ast::comparable::ComparableExpr;
use crate::ast::helpers::{
contains_call_path, create_expr, create_stmt, unparse_expr, unparse_stmt,
contains_call_path, create_expr, create_stmt, has_comments, unparse_expr, unparse_stmt,
};
use crate::ast::types::Range;
use crate::autofix::Fix;
@@ -201,14 +202,127 @@ pub fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: Option<&
let target_var = &body_targets[0];
let ternary = ternary(target_var, body_value, test, orelse_value);
let content = unparse_stmt(&ternary, checker.style);
let contents = unparse_stmt(&ternary, checker.style);
// Don't flag for simplified ternaries if the resulting expression would exceed
// the maximum line length.
if stmt.location.column() + contents.len() > checker.settings.line_length {
return;
}
// Don't flag for simplified ternaries if the if-expression contains any
// comments.
if has_comments(stmt, checker.locator) {
return;
}
let mut diagnostic = Diagnostic::new(
violations::UseTernaryOperator(content.clone()),
violations::UseTernaryOperator(contents.clone()),
Range::from_located(stmt),
);
if checker.patch(&RuleCode::SIM108) {
diagnostic.amend(Fix::replacement(
content,
contents,
stmt.location,
stmt.end_location.unwrap(),
));
}
checker.diagnostics.push(diagnostic);
}
fn compare_expr(expr1: &ComparableExpr, expr2: &ComparableExpr) -> bool {
expr1.eq(expr2)
}
/// SIM401
pub fn use_dict_get_with_default(
checker: &mut Checker,
stmt: &Stmt,
test: &Expr,
body: &Vec<Stmt>,
orelse: &Vec<Stmt>,
) {
if body.len() != 1 || orelse.len() != 1 {
return;
}
let StmtKind::Assign { targets: body_var, value: body_val, ..} = &body[0].node else {
return;
};
if body_var.len() != 1 {
return;
};
let StmtKind::Assign { targets: orelse_var, value: orelse_val, .. } = &orelse[0].node else {
return;
};
if orelse_var.len() != 1 {
return;
};
let ExprKind::Compare { left: test_key, ops , comparators: test_dict } = &test.node else {
return;
};
if test_dict.len() != 1 {
return;
}
let (expected_var, expected_val, default_var, default_val) = match ops[..] {
[Cmpop::In] => (&body_var[0], body_val, &orelse_var[0], orelse_val),
[Cmpop::NotIn] => (&orelse_var[0], orelse_val, &body_var[0], body_val),
_ => {
return;
}
};
let test_dict = &test_dict[0];
let ExprKind::Subscript { value: expected_subscript, slice: expected_slice, .. } = &expected_val.node else {
return;
};
// Check that the dictionary key, target variables, and dictionary name are all
// equivalent.
if !compare_expr(&expected_slice.into(), &test_key.into())
|| !compare_expr(&expected_var.into(), &default_var.into())
|| !compare_expr(&test_dict.into(), &expected_subscript.into())
{
return;
}
let contents = unparse_stmt(
&create_stmt(StmtKind::Assign {
targets: vec![create_expr(expected_var.node.clone())],
value: Box::new(create_expr(ExprKind::Call {
func: Box::new(create_expr(ExprKind::Attribute {
value: expected_subscript.clone(),
attr: "get".to_string(),
ctx: ExprContext::Load,
})),
args: vec![
create_expr(test_key.node.clone()),
create_expr(default_val.node.clone()),
],
keywords: vec![],
})),
type_comment: None,
}),
checker.style,
);
// Don't flag for simplified `dict.get` if the resulting expression would exceed
// the maximum line length.
if stmt.location.column() + contents.len() > checker.settings.line_length {
return;
}
// Don't flag for simplified `dict.get` if the if-expression contains any
// comments.
if has_comments(stmt, checker.locator) {
return;
}
let mut diagnostic = Diagnostic::new(
violations::DictGetWithDefault(contents.clone()),
Range::from_located(stmt),
);
if checker.patch(&RuleCode::SIM401) {
diagnostic.amend(Fix::replacement(
contents,
stmt.location,
stmt.end_location.unwrap(),
));

View File

@@ -1,25 +1,32 @@
pub use ast_bool_op::{
a_and_not_a, a_or_not_a, and_false, compare_with_tuple, duplicate_isinstance_call, or_true,
};
pub use ast_expr::use_capital_environment_variables;
pub use ast_for::convert_loop_to_any_all;
pub use ast_if::{nested_if_statements, return_bool_condition_directly, use_ternary_operator};
pub use ast_if::{
nested_if_statements, return_bool_condition_directly, use_dict_get_with_default,
use_ternary_operator,
};
pub use ast_ifexp::{
explicit_false_true_in_ifexpr, explicit_true_false_in_ifexpr, twisted_arms_in_ifexpr,
};
pub use ast_unary_op::{double_negation, negation_with_equal_op, negation_with_not_equal_op};
pub use ast_with::multiple_with_statements;
pub use key_in_dict::{key_in_dict_compare, key_in_dict_for};
pub use open_file_with_context_handler::open_file_with_context_handler;
pub use return_in_try_except_finally::return_in_try_except_finally;
pub use use_contextlib_suppress::use_contextlib_suppress;
pub use yoda_conditions::yoda_conditions;
mod ast_bool_op;
mod ast_expr;
mod ast_for;
mod ast_if;
mod ast_ifexp;
mod ast_unary_op;
mod ast_with;
mod key_in_dict;
mod open_file_with_context_handler;
mod return_in_try_except_finally;
mod use_contextlib_suppress;
mod yoda_conditions;

View File

@@ -0,0 +1,30 @@
use rustpython_ast::Expr;
use rustpython_parser::ast::StmtKind;
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violations;
/// SIM115
pub fn open_file_with_context_handler(checker: &mut Checker, func: &Expr) {
if match_call_path(
&dealias_call_path(collect_call_paths(func), &checker.import_aliases),
"",
"open",
&checker.from_imports,
) {
if checker.is_builtin("open") {
match checker.current_stmt().node {
StmtKind::With { .. } => (),
_ => {
checker.diagnostics.push(Diagnostic::new(
violations::OpenFileWithContextHandler,
Range::from_located(func),
));
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
---
source: src/flake8_simplify/mod.rs
expression: checks
expression: diagnostics
---
- kind:
UseTernaryOperator: b = c if a else d
@@ -19,4 +19,21 @@ expression: checks
row: 5
column: 9
parent: ~
- kind:
UseTernaryOperator: b = cccccccccccccccccccccccccccccccccccc if a else ddddddddddddddddddddddddddddddddddddd
location:
row: 82
column: 0
end_location:
row: 85
column: 45
fix:
content: b = cccccccccccccccccccccccccccccccccccc if a else ddddddddddddddddddddddddddddddddddddd
location:
row: 82
column: 0
end_location:
row: 85
column: 45
parent: ~

View File

@@ -0,0 +1,81 @@
---
source: src/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
UseCapitalEnvironmentVariables:
- FOO
- foo
location:
row: 4
column: 11
end_location:
row: 4
column: 16
fix:
content: "'FOO'"
location:
row: 4
column: 11
end_location:
row: 4
column: 16
parent: ~
- kind:
UseCapitalEnvironmentVariables:
- FOO
- foo
location:
row: 6
column: 15
end_location:
row: 6
column: 20
fix:
content: "'FOO'"
location:
row: 6
column: 15
end_location:
row: 6
column: 20
parent: ~
- kind:
UseCapitalEnvironmentVariables:
- FOO
- foo
location:
row: 8
column: 15
end_location:
row: 8
column: 20
fix:
content: "'FOO'"
location:
row: 8
column: 15
end_location:
row: 8
column: 20
parent: ~
- kind:
UseCapitalEnvironmentVariables:
- FOO
- foo
location:
row: 10
column: 10
end_location:
row: 10
column: 15
fix:
content: "'FOO'"
location:
row: 10
column: 10
end_location:
row: 10
column: 15
parent: ~

View File

@@ -0,0 +1,15 @@
---
source: src/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
OpenFileWithContextHandler: ~
location:
row: 1
column: 4
end_location:
row: 1
column: 8
fix: ~
parent: ~

View File

@@ -0,0 +1,107 @@
---
source: src/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
DictGetWithDefault: "var = a_dict.get(key, \"default1\")"
location:
row: 6
column: 0
end_location:
row: 9
column: 20
fix:
content: "var = a_dict.get(key, \"default1\")"
location:
row: 6
column: 0
end_location:
row: 9
column: 20
parent: ~
- kind:
DictGetWithDefault: "var = a_dict.get(key, \"default2\")"
location:
row: 12
column: 0
end_location:
row: 15
column: 21
fix:
content: "var = a_dict.get(key, \"default2\")"
location:
row: 12
column: 0
end_location:
row: 15
column: 21
parent: ~
- kind:
DictGetWithDefault: "var = a_dict.get(key, val1 + val2)"
location:
row: 18
column: 0
end_location:
row: 21
column: 21
fix:
content: "var = a_dict.get(key, val1 + val2)"
location:
row: 18
column: 0
end_location:
row: 21
column: 21
parent: ~
- kind:
DictGetWithDefault: "var = a_dict.get(keys[idx], \"default\")"
location:
row: 24
column: 0
end_location:
row: 27
column: 19
fix:
content: "var = a_dict.get(keys[idx], \"default\")"
location:
row: 24
column: 0
end_location:
row: 27
column: 19
parent: ~
- kind:
DictGetWithDefault: "var = dicts[idx].get(key, \"default\")"
location:
row: 30
column: 0
end_location:
row: 33
column: 19
fix:
content: "var = dicts[idx].get(key, \"default\")"
location:
row: 30
column: 0
end_location:
row: 33
column: 19
parent: ~
- kind:
DictGetWithDefault: "vars[idx] = a_dict.get(key, \"default\")"
location:
row: 36
column: 0
end_location:
row: 39
column: 25
fix:
content: "vars[idx] = a_dict.get(key, \"default\")"
location:
row: 36
column: 0
end_location:
row: 39
column: 25
parent: ~

View File

@@ -1,19 +1,6 @@
use rustpython_ast::{Constant, ExprKind, Stmt, StmtKind};
/// Return `true` if a `Stmt` is a docstring.
fn is_docstring_stmt(stmt: &Stmt) -> bool {
if let StmtKind::Expr { value } = &stmt.node {
matches!(
value.node,
ExprKind::Constant {
value: Constant::Str { .. },
..
}
)
} else {
false
}
}
use crate::ast::helpers::is_docstring_stmt;
/// Return `true` if a `Stmt` is a "empty": a `pass`, `...`, `raise
/// NotImplementedError`, or `raise NotImplemented` (with or without arguments).

View File

@@ -153,6 +153,10 @@ pub fn unused_arguments(
.enabled
.contains(Argumentable::Method.rule_code())
&& !helpers::is_empty(body)
&& (!visibility::is_magic(name)
|| visibility::is_init(name)
|| visibility::is_new(name)
|| visibility::is_call(name))
&& !visibility::is_abstract(checker, decorator_list)
&& !visibility::is_override(checker, decorator_list)
&& !visibility::is_overload(checker, decorator_list)
@@ -178,6 +182,10 @@ pub fn unused_arguments(
.enabled
.contains(Argumentable::ClassMethod.rule_code())
&& !helpers::is_empty(body)
&& (!visibility::is_magic(name)
|| visibility::is_init(name)
|| visibility::is_new(name)
|| visibility::is_call(name))
&& !visibility::is_abstract(checker, decorator_list)
&& !visibility::is_override(checker, decorator_list)
&& !visibility::is_overload(checker, decorator_list)
@@ -203,6 +211,10 @@ pub fn unused_arguments(
.enabled
.contains(Argumentable::StaticMethod.rule_code())
&& !helpers::is_empty(body)
&& (!visibility::is_magic(name)
|| visibility::is_init(name)
|| visibility::is_new(name)
|| visibility::is_call(name))
&& !visibility::is_abstract(checker, decorator_list)
&& !visibility::is_override(checker, decorator_list)
&& !visibility::is_overload(checker, decorator_list)

View File

@@ -1,6 +1,6 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
expression: diagnostics
---
- kind:
UnusedMethodArgument: x
@@ -32,4 +32,14 @@ expression: checks
column: 16
fix: ~
parent: ~
- kind:
UnusedMethodArgument: x
location:
row: 190
column: 23
end_location:
row: 190
column: 24
fix: ~
parent: ~

View File

@@ -1,7 +1,8 @@
use rustpython_ast::Stmt;
use rustpython_ast::{Location, Stmt};
use rustpython_parser::lexer;
use rustpython_parser::lexer::Tok;
use crate::ast::helpers::is_docstring_stmt;
use crate::ast::types::Range;
use crate::isort::types::TrailingComma;
use crate::source_code_locator::SourceCodeLocator;
@@ -86,3 +87,122 @@ pub fn has_comment_break(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
}
false
}
/// Find the end of the last docstring.
fn match_docstring_end(body: &[Stmt]) -> Option<Location> {
let mut iter = body.iter();
let Some(mut stmt) = iter.next() else {
return None;
};
if !is_docstring_stmt(stmt) {
return None;
}
for next in iter {
if !is_docstring_stmt(next) {
break;
}
stmt = next;
}
Some(stmt.end_location.unwrap())
}
/// Find the end of the first token that isn't a docstring, comment, or
/// whitespace.
pub fn find_splice_location(body: &[Stmt], locator: &SourceCodeLocator) -> Location {
// Find the first AST node that isn't a docstring.
let mut splice = match_docstring_end(body).unwrap_or_default();
// Find the first token that isn't a comment or whitespace.
let contents = locator.slice_source_code_at(&splice);
for (.., tok, end) in lexer::make_tokenizer(&contents).flatten() {
if matches!(tok, Tok::Comment(..) | Tok::Newline) {
splice = end;
} else {
break;
}
}
splice
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use rustpython_ast::Location;
use rustpython_parser::parser;
use crate::isort::helpers::find_splice_location;
use crate::source_code_locator::SourceCodeLocator;
fn splice_contents(contents: &str) -> Result<Location> {
let program = parser::parse_program(contents, "<filename>")?;
let locator = SourceCodeLocator::new(contents);
Ok(find_splice_location(&program, &locator))
}
#[test]
fn splice() -> Result<()> {
let contents = "";
assert_eq!(splice_contents(contents)?, Location::new(1, 0));
let contents = r#"
"""Hello, world!"""
"#
.trim();
assert_eq!(splice_contents(contents)?, Location::new(1, 19));
let contents = r#"
"""Hello, world!"""
"""Hello, world!"""
"#
.trim();
assert_eq!(splice_contents(contents)?, Location::new(2, 19));
let contents = r#"
x = 1
"#
.trim();
assert_eq!(splice_contents(contents)?, Location::new(1, 0));
let contents = r#"
#!/usr/bin/env python3
"#
.trim();
assert_eq!(splice_contents(contents)?, Location::new(1, 22));
let contents = r#"
#!/usr/bin/env python3
"""Hello, world!"""
"#
.trim();
assert_eq!(splice_contents(contents)?, Location::new(2, 19));
let contents = r#"
"""Hello, world!"""
#!/usr/bin/env python3
"#
.trim();
assert_eq!(splice_contents(contents)?, Location::new(2, 22));
let contents = r#"
"""%s""" % "Hello, world!"
"#
.trim();
assert_eq!(splice_contents(contents)?, Location::new(1, 0));
let contents = r#"
"""Hello, world!"""; x = 1
"#
.trim();
assert_eq!(splice_contents(contents)?, Location::new(1, 19));
let contents = r#"
"""Hello, world!"""; x = 1; y = \
2
"#
.trim();
assert_eq!(splice_contents(contents)?, Location::new(1, 19));
Ok(())
}
}

View File

@@ -828,4 +828,99 @@ mod tests {
insta::assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("docstring.py"))]
#[test_case(Path::new("docstring_only.py"))]
#[test_case(Path::new("empty.py"))]
fn required_import(path: &Path) -> Result<()> {
let snapshot = format!("required_import_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/isort/required_imports")
.join(path)
.as_path(),
&Settings {
src: vec![Path::new("resources/test/fixtures/isort").to_path_buf()],
isort: isort::settings::Settings {
required_imports: BTreeSet::from([
"from __future__ import annotations".to_string()
]),
..isort::settings::Settings::default()
},
..Settings::for_rule(RuleCode::I002)
},
)?;
insta::assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("docstring.py"))]
#[test_case(Path::new("docstring_only.py"))]
#[test_case(Path::new("empty.py"))]
fn required_imports(path: &Path) -> Result<()> {
let snapshot = format!("required_imports_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/isort/required_imports")
.join(path)
.as_path(),
&Settings {
src: vec![Path::new("resources/test/fixtures/isort").to_path_buf()],
isort: isort::settings::Settings {
required_imports: BTreeSet::from([
"from __future__ import annotations".to_string(),
"from __future__ import generator_stop".to_string(),
]),
..isort::settings::Settings::default()
},
..Settings::for_rule(RuleCode::I002)
},
)?;
insta::assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("docstring.py"))]
#[test_case(Path::new("docstring_only.py"))]
#[test_case(Path::new("empty.py"))]
fn combined_required_imports(path: &Path) -> Result<()> {
let snapshot = format!("combined_required_imports_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/isort/required_imports")
.join(path)
.as_path(),
&Settings {
src: vec![Path::new("resources/test/fixtures/isort").to_path_buf()],
isort: isort::settings::Settings {
required_imports: BTreeSet::from(["from __future__ import annotations, \
generator_stop"
.to_string()]),
..isort::settings::Settings::default()
},
..Settings::for_rule(RuleCode::I002)
},
)?;
insta::assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("docstring.py"))]
#[test_case(Path::new("docstring_only.py"))]
#[test_case(Path::new("empty.py"))]
fn straight_required_import(path: &Path) -> Result<()> {
let snapshot = format!("straight_required_import_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/isort/required_imports")
.join(path)
.as_path(),
&Settings {
src: vec![Path::new("resources/test/fixtures/isort").to_path_buf()],
isort: isort::settings::Settings {
required_imports: BTreeSet::from(["import os".to_string()]),
..isort::settings::Settings::default()
},
..Settings::for_rule(RuleCode::I002)
},
)?;
insta::assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -0,0 +1,222 @@
use std::fmt;
use log::error;
use rustpython_ast::{Location, StmtKind, Suite};
use crate::ast::helpers::is_docstring_stmt;
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::isort::helpers;
use crate::isort::track::Block;
use crate::registry::{Diagnostic, RuleCode};
use crate::settings::{flags, Settings};
use crate::source_code_locator::SourceCodeLocator;
use crate::violations;
struct Alias<'a> {
name: &'a str,
as_name: Option<&'a str>,
}
struct ImportFrom<'a> {
module: Option<&'a str>,
name: Alias<'a>,
level: Option<&'a usize>,
}
struct Import<'a> {
name: Alias<'a>,
}
enum AnyImport<'a> {
Import(Import<'a>),
ImportFrom(ImportFrom<'a>),
}
impl fmt::Display for ImportFrom<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "from ")?;
if let Some(level) = self.level {
write!(f, "{}", ".".repeat(*level))?;
}
if let Some(module) = self.module {
write!(f, "{module}")?;
}
write!(f, " import {}", self.name.name)?;
Ok(())
}
}
impl fmt::Display for Import<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "import {}", self.name.name)?;
if let Some(as_name) = self.name.as_name {
write!(f, " as {as_name}")?;
}
Ok(())
}
}
impl fmt::Display for AnyImport<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AnyImport::Import(import) => write!(f, "{import}"),
AnyImport::ImportFrom(import_from) => write!(f, "{import_from}"),
}
}
}
fn contains(block: &Block, required_import: &AnyImport) -> bool {
block.imports.iter().any(|import| match required_import {
AnyImport::Import(required_import) => {
let StmtKind::Import {
names,
} = &import.node else {
return false;
};
names.iter().any(|alias| {
alias.node.name == required_import.name.name
&& alias.node.asname.as_deref() == required_import.name.as_name
})
}
AnyImport::ImportFrom(required_import) => {
let StmtKind::ImportFrom {
module,
names,
level,
} = &import.node else {
return false;
};
module.as_deref() == required_import.module
&& level.as_ref() == required_import.level
&& names.iter().any(|alias| {
alias.node.name == required_import.name.name
&& alias.node.asname.as_deref() == required_import.name.as_name
})
}
})
}
fn add_required_import(
required_import: &AnyImport,
blocks: &[&Block],
python_ast: &Suite,
locator: &SourceCodeLocator,
settings: &Settings,
autofix: flags::Autofix,
) -> Option<Diagnostic> {
// If the import is already present in a top-level block, don't add it.
if blocks
.iter()
.filter(|block| !block.nested)
.any(|block| contains(block, required_import))
{
return None;
}
// Don't add imports to semantically-empty files.
if python_ast.iter().all(is_docstring_stmt) {
return None;
}
// Always insert the diagnostic at top-of-file.
let required_import = required_import.to_string();
let mut diagnostic = Diagnostic::new(
violations::MissingRequiredImport(required_import.clone()),
Range::new(Location::default(), Location::default()),
);
if matches!(autofix, flags::Autofix::Enabled) && settings.fixable.contains(&RuleCode::I002) {
// Determine the location at which the import should be inserted.
let splice = helpers::find_splice_location(python_ast, locator);
// Generate the edit.
let mut contents = String::with_capacity(required_import.len() + 1);
// If we're inserting beyond the start of the file, we add
// a newline _before_, since the splice represents the _end_ of the last
// irrelevant token (e.g., the end of a comment or the end of
// docstring). This ensures that we properly handle awkward cases like
// docstrings that are followed by semicolons.
if splice > Location::default() {
contents.push('\n');
}
contents.push_str(&required_import);
// If we're inserting at the start of the file, add a trailing newline instead.
if splice == Location::default() {
contents.push('\n');
}
// Construct the fix.
diagnostic.amend(Fix::insertion(contents, splice));
}
Some(diagnostic)
}
/// I002
pub fn add_required_imports(
blocks: &[&Block],
python_ast: &Suite,
locator: &SourceCodeLocator,
settings: &Settings,
autofix: flags::Autofix,
) -> Vec<Diagnostic> {
settings
.isort
.required_imports
.iter()
.flat_map(|required_import| {
let Ok(body) = rustpython_parser::parser::parse_program(required_import, "<filename>") else {
error!("Failed to parse required import: `{}`", required_import);
return vec![];
};
if body.is_empty() || body.len() > 1 {
error!("Expected require import to contain a single statement: `{}`", required_import);
return vec![];
}
match &body[0].node {
StmtKind::ImportFrom { module, names, level } => {
names.iter().filter_map(|name| {
add_required_import(
&AnyImport::ImportFrom(ImportFrom {
module: module.as_ref().map(String::as_str),
name: Alias {
name: name.node.name.as_str(),
as_name: name.node.asname.as_deref(),
},
level: level.as_ref(),
}),
blocks,
python_ast,
locator,
settings,
autofix,
)
}).collect()
}
StmtKind::Import { names } => {
names.iter().filter_map(|name| {
add_required_import(
&AnyImport::Import(Import {
name: Alias {
name: name.node.name.as_str(),
as_name: name.node.asname.as_deref(),
},
}),
blocks,
python_ast,
locator,
settings,
autofix,
)
}).collect()
}
_ => {
error!("Expected required import to be in import-from style: `{}`", required_import);
vec![]
}
}
})
.collect()
}

5
src/isort/rules/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub use add_required_imports::add_required_imports;
pub use organize_imports::organize_imports;
pub mod add_required_imports;
pub mod organize_imports;

View File

@@ -27,7 +27,7 @@ fn extract_indentation_range(body: &[&Stmt]) -> Range {
}
/// I001
pub fn check_imports(
pub fn organize_imports(
block: &Block,
locator: &SourceCodeLocator,
settings: &Settings,

View File

@@ -129,36 +129,47 @@ pub struct Options {
/// A list of modules to consider standard-library, in addition to those
/// known to Ruff in advance.
pub extra_standard_library: Option<Vec<String>>,
#[option(
default = r#"[]"#,
value_type = "Vec<String>",
example = r#"
add-import = ["from __future__ import annotations"]
"#
)]
/// Add the specified import line to all files.
pub required_imports: Option<Vec<String>>,
}
#[derive(Debug, Hash)]
#[allow(clippy::struct_excessive_bools)]
pub struct Settings {
pub required_imports: BTreeSet<String>,
pub combine_as_imports: bool,
pub force_wrap_aliases: bool,
pub split_on_trailing_comma: bool,
pub extra_standard_library: BTreeSet<String>,
pub force_single_line: bool,
pub order_by_type: bool,
pub force_sort_within_sections: bool,
pub single_line_exclusions: BTreeSet<String>,
pub force_wrap_aliases: bool,
pub known_first_party: BTreeSet<String>,
pub known_third_party: BTreeSet<String>,
pub extra_standard_library: BTreeSet<String>,
pub order_by_type: bool,
pub single_line_exclusions: BTreeSet<String>,
pub split_on_trailing_comma: bool,
}
impl Default for Settings {
fn default() -> Self {
Self {
required_imports: BTreeSet::new(),
combine_as_imports: false,
force_wrap_aliases: false,
split_on_trailing_comma: true,
extra_standard_library: BTreeSet::new(),
force_single_line: false,
order_by_type: true,
force_sort_within_sections: false,
single_line_exclusions: BTreeSet::new(),
force_wrap_aliases: false,
known_first_party: BTreeSet::new(),
known_third_party: BTreeSet::new(),
extra_standard_library: BTreeSet::new(),
order_by_type: true,
single_line_exclusions: BTreeSet::new(),
split_on_trailing_comma: true,
}
}
}
@@ -166,20 +177,21 @@ impl Default for Settings {
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
required_imports: BTreeSet::from_iter(options.required_imports.unwrap_or_default()),
combine_as_imports: options.combine_as_imports.unwrap_or(false),
force_wrap_aliases: options.force_wrap_aliases.unwrap_or(false),
split_on_trailing_comma: options.split_on_trailing_comma.unwrap_or(true),
force_single_line: options.force_single_line.unwrap_or(false),
order_by_type: options.order_by_type.unwrap_or(true),
force_sort_within_sections: options.force_sort_within_sections.unwrap_or(false),
single_line_exclusions: BTreeSet::from_iter(
options.single_line_exclusions.unwrap_or_default(),
),
known_first_party: BTreeSet::from_iter(options.known_first_party.unwrap_or_default()),
known_third_party: BTreeSet::from_iter(options.known_third_party.unwrap_or_default()),
extra_standard_library: BTreeSet::from_iter(
options.extra_standard_library.unwrap_or_default(),
),
force_single_line: options.force_single_line.unwrap_or(false),
force_sort_within_sections: options.force_sort_within_sections.unwrap_or(false),
force_wrap_aliases: options.force_wrap_aliases.unwrap_or(false),
known_first_party: BTreeSet::from_iter(options.known_first_party.unwrap_or_default()),
known_third_party: BTreeSet::from_iter(options.known_third_party.unwrap_or_default()),
order_by_type: options.order_by_type.unwrap_or(true),
single_line_exclusions: BTreeSet::from_iter(
options.single_line_exclusions.unwrap_or_default(),
),
split_on_trailing_comma: options.split_on_trailing_comma.unwrap_or(true),
}
}
}
@@ -187,16 +199,17 @@ impl From<Options> for Settings {
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
required_imports: Some(settings.required_imports.into_iter().collect()),
combine_as_imports: Some(settings.combine_as_imports),
force_wrap_aliases: Some(settings.force_wrap_aliases),
split_on_trailing_comma: Some(settings.split_on_trailing_comma),
extra_standard_library: Some(settings.extra_standard_library.into_iter().collect()),
force_single_line: Some(settings.force_single_line),
order_by_type: Some(settings.order_by_type),
force_sort_within_sections: Some(settings.force_sort_within_sections),
single_line_exclusions: Some(settings.single_line_exclusions.into_iter().collect()),
force_wrap_aliases: Some(settings.force_wrap_aliases),
known_first_party: Some(settings.known_first_party.into_iter().collect()),
known_third_party: Some(settings.known_third_party.into_iter().collect()),
extra_standard_library: Some(settings.extra_standard_library.into_iter().collect()),
order_by_type: Some(settings.order_by_type),
single_line_exclusions: Some(settings.single_line_exclusions.into_iter().collect()),
split_on_trailing_comma: Some(settings.split_on_trailing_comma),
}
}
}

View File

@@ -0,0 +1,39 @@
---
source: src/isort/mod.rs
expression: diagnostics
---
- kind:
MissingRequiredImport: from __future__ import annotations
location:
row: 1
column: 0
end_location:
row: 1
column: 0
fix:
content: "\nfrom __future__ import annotations"
location:
row: 1
column: 19
end_location:
row: 1
column: 19
parent: ~
- kind:
MissingRequiredImport: from __future__ import generator_stop
location:
row: 1
column: 0
end_location:
row: 1
column: 0
fix:
content: "\nfrom __future__ import generator_stop"
location:
row: 1
column: 19
end_location:
row: 1
column: 19
parent: ~

View File

@@ -0,0 +1,6 @@
---
source: src/isort/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/isort/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: diagnostics
---
- kind:
MissingRequiredImport: from __future__ import annotations
location:
row: 1
column: 0
end_location:
row: 1
column: 0
fix:
content: "\nfrom __future__ import annotations"
location:
row: 1
column: 19
end_location:
row: 1
column: 19
parent: ~

View File

@@ -0,0 +1,6 @@
---
source: src/isort/mod.rs
expression: diagnostics
---
[]

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