Compare commits

...

79 Commits

Author SHA1 Message Date
Charlie Marsh
11e1380df4 Bump version to 0.0.265 (#4248) 2023-05-05 13:16:05 -04:00
Micha Reiser
e93f378635 Refactor whitespace around operator (#4223) 2023-05-05 09:37:56 +02:00
Micha Reiser
2124feb0e7 Fail lint tests if the fix creates a syntax error (#4202) 2023-05-05 07:59:33 +02:00
Charlie Marsh
c0e7269b07 Update doc defaults for section-order (#4232) 2023-05-04 21:35:27 +00:00
Chris Chan
c2921e957b [pylint] Implement import-self (W0406) (#4154) 2023-05-04 16:05:15 -04:00
Charlie Marsh
93cfce674a Ignore __debuggerskip__ in unused variable checks (#4229) 2023-05-04 15:45:49 -04:00
Charlie Marsh
b71cc3789f Change --fix-only exit semantics to mirror --fix (#4146) 2023-05-04 19:03:15 +00:00
Zanie Adkins
717128112d Fix panic in pydocstyle D214 when docstring indentation is empty (#4216) 2023-05-04 14:42:34 -04:00
Arya Kumar
e9e194ab32 [flake8-pyi] Implement PYI042 and PYI043 (#4214) 2023-05-04 14:35:26 -04:00
Calum Young
890e630c41 Allow linking to individual rules (#4158) 2023-05-04 13:43:53 -04:00
Aaron Cunningham
d78287540d Update B027 to support autofixing (#4178) 2023-05-04 16:36:32 +00:00
Charlie Marsh
494e807315 Add space when joining rule codes for debug messages (#4225) 2023-05-04 15:34:34 +00:00
Tom Kuson
6db1a32eb9 Add docs for PLC rules (#4224) 2023-05-04 10:56:00 -04:00
Dhruv Manilawala
bb2cbf1f25 End of statement insertion should occur after newline (#4215) 2023-05-04 16:17:41 +02:00
konstin
badfdab61a Show rule codes on autofix failure (#4220) 2023-05-04 15:25:07 +02:00
Dhruv Manilawala
59d40f9f81 Show settings path in --show-settings output (#4199) 2023-05-04 08:22:31 +02:00
Arya Kumar
37aae666c7 [flake8-pyi] PYI020 (#4211) 2023-05-03 22:37:32 -04:00
Leiser Fernández Gallo
460023a959 Fix era panic caused by out of bound edition (#4206) 2023-05-03 15:48:43 +02:00
Aarni Koskela
d0e3ca29d9 Print out autofix-broken or non-converging code when debugging (#4201) 2023-05-03 13:50:03 +02:00
Christian Clauss
ccfc78e2d5 faq: Clarify how Ruff and Black treat line-length. (#4180) 2023-05-02 23:19:38 +00:00
Micha Reiser
b14358fbfe Render tabs as 4 spaces in diagnostics (#4132) 2023-05-02 13:14:02 +00:00
wookie184
ac600bb3da Warn on PEP 604 syntax not in an annotation, but don't autofix (#4170) 2023-05-01 23:49:20 -07:00
Charlie Marsh
8cb76f85eb Bump version to 0.0.264 (#4179) 2023-05-01 23:33:38 -07:00
Charlie Marsh
56c45013c2 Allow boolean parameters for pytest.param (#4176) 2023-05-02 01:07:50 +00:00
Calum Young
a4ce746892 Reference related settings in rules (#4157) 2023-05-02 00:59:00 +00:00
Calum Young
2d6d51f3a1 Add flake8-return docs (#4164) 2023-05-02 00:53:46 +00:00
Jonathan Plasse
814731364a Fix UP032 auto-fix (#4165) 2023-04-30 16:57:41 -04:00
Jonathan Plasse
8c97e7922b Fix F811 false positive with match (#4161) 2023-04-30 14:39:45 -04:00
Jonathan Plasse
a32617911a Use --filter=blob:none to clone CPython faster (#4156) 2023-04-30 13:39:22 +02:00
Charlie Marsh
64b7280eb8 Respect parent-scoping rules for NamedExpr assignments (#4145) 2023-04-29 22:45:30 +00:00
Evan Rittenhouse
8d64747d34 Remove pyright comment prefix from PYI033 checks (#4152) 2023-04-29 18:41:04 -04:00
Charlie Marsh
2115d99c43 Remove ScopeStack in favor of child-parent ScopeId pointers (#4138) 2023-04-29 18:23:51 -04:00
Calum Young
39ed75f643 Document flake8-unused-arguments (#4147) 2023-04-29 19:17:50 +00:00
Calum Young
8f61eae1e7 Add remaining pep8-naming docs (#4149) 2023-04-29 15:13:10 -04:00
Calum Young
f0f4bf2929 Move typos to pre-commit config (#4148) 2023-04-29 12:13:35 -04:00
Calum Young
03144b2fad Document flake8-commas (#4142) 2023-04-29 03:24:15 +00:00
Calum Young
0172cc51a7 Document flake8-print (#4144) 2023-04-29 03:19:00 +00:00
Calum Young
12d64a223b Document RUF100 (#4141) 2023-04-28 22:14:15 +00:00
Charlie Marsh
432ea6f2e2 Tweak rule documentation for B008 (#4137) 2023-04-28 01:29:03 +00:00
Evan Rittenhouse
b34804ceb5 Make D410/D411 autofixes mutually exclusive (#4110) 2023-04-28 01:24:35 +00:00
Moritz Sauter
ee6d8f7467 Add bugbear immutable functions as allowed in dataclasses (#4122) 2023-04-27 21:23:06 -04:00
Dhruv Manilawala
089b64e9c1 Autofix EM101, EM102, EM103 if possible (#4123) 2023-04-27 18:53:27 +00:00
Tom Kuson
3e81403fbe Add pygrep-hooks documentation (#4131) 2023-04-27 18:33:07 +00:00
Charlie Marsh
3c9f5e2fdc Preserve star-handling special-casing for force-single-line (#4129) 2023-04-27 00:02:17 -04:00
Micha Reiser
17db2e2a62 Fix B023 shadowed variables in nested functions (#4111) 2023-04-26 22:01:31 +01:00
Micha Reiser
e04ef42334 Use memchr to speedup newline search on x86 (#3985) 2023-04-26 20:15:47 +01:00
Micha Reiser
f3e6ddda62 perf(logical-lines): Various small perf improvements (#4022) 2023-04-26 20:10:35 +01:00
Micha Reiser
cab65b25da Replace row/column based Location with byte-offsets. (#3931) 2023-04-26 18:11:02 +00:00
Charlie Marsh
ee91598835 Tweak --show-fixes documentation (#4117) 2023-04-26 15:15:56 +00:00
Calum Young
ab65eaea7f Add docs build validation stage to CI (#4116)
Nice. Thank you
2023-04-26 14:57:59 +01:00
konstin
19d8913e32 Use musl in ecosystem docker (#3998)
This prevents errors when the host glibc is newer than the one in the docker container
2023-04-26 05:54:53 +02:00
Dhruv Manilawala
b9c06b48e1 Document that --diff implies --fix-only (#4098) 2023-04-25 21:19:44 -06:00
Charlie Marsh
7266eb0d69 Add support for providing command-line arguments via argfile (#4087) 2023-04-25 17:58:21 -06:00
Jonathan Plasse
4df7bc0bcd Fix E713 and E714 false positives for multiple comparisons (#4083) 2023-04-25 11:37:56 -06:00
Calum Young
464a0ff483 Fix docs failure (#4097) 2023-04-25 11:30:37 -06:00
Charlie Marsh
fd7ccb4c9e Bump version to 0.0.263 (#4086) 2023-04-24 23:32:29 -06:00
Evan Rittenhouse
ae6f38344a Unify positional and keyword arguments when checking for missing arguments in docstring (#4067) 2023-04-25 05:32:15 +00:00
Trevor McCulloch
bbf658d4c5 [pylint] Implement PLE0302 unexpected-special-method-signature (#4075) 2023-04-25 04:51:21 +00:00
Jonathan Plasse
1f3b0fd602 Fix SIM222 and SIM223 false positives and auto-fix (#4063) 2023-04-25 04:44:02 +00:00
Dhruv Manilawala
37483f3ac9 Ignore ClassVar annotation for RUF008, RUF009 (#4081) 2023-04-24 23:58:30 +00:00
Zanie Adkins
4d3a1e0581 Add PrefectHQ/prefect to list of ruff users (#4084) 2023-04-24 17:49:12 -06:00
Bartosz Sokorski
9e5f348a17 Add Poetry to the list of projects using Ruff (#4085) 2023-04-24 17:48:35 -06:00
Jonathan Plasse
5e91211e6d Add in_boolean_test to Context (#4072) 2023-04-23 23:18:23 -06:00
Jonathan Plasse
df77595426 Move Truthiness into ruff_python_ast (#4071) 2023-04-24 04:54:31 +00:00
Charlie Marsh
407af6e0ae Avoid infinite-propagation of inline comments when force-splitting imports (#4074) 2023-04-23 22:39:51 -06:00
Dhruv Manilawala
d64146683e Increment priority should be (branch-local, global) (#4070) 2023-04-23 00:04:15 -06:00
Charlie Marsh
0e7914010f Misc. small clean-up of flake8-import-conventions rules (#4069) 2023-04-23 04:57:15 +00:00
Edgar R. M
cfc7d8a2b5 [flake8-import-conventions] Implement new rule ICN003 to ban from ... import ... for selected modules (#4040) 2023-04-23 04:40:36 +00:00
Tom Kuson
f5cd659292 Add docs for tryceratops rules (#4042) 2023-04-23 04:35:56 +00:00
Charlie Marsh
260138b427 Use Context for pep8-naming helpers (#4068) 2023-04-22 18:44:54 -04:00
Jonathan Plasse
2da149fd7e Ignore N815 for TypedDict fields (#4066) 2023-04-22 18:17:14 -04:00
Micha Reiser
e33887718d Use Rust 1.69 (#4065) 2023-04-22 23:04:17 +01:00
Micha Reiser
ba4f4f4672 Upgrade dependencies (#4064) 2023-04-22 18:04:01 +01:00
Pronoy Mandal
b7a57ce120 Update tutorial.md (#4055) 2023-04-21 10:56:31 -06:00
Alan Du
82abbc7234 [flake8-bugbear] Add pytest.raises(Exception) support to B017 (#4052) 2023-04-21 03:43:01 +00:00
Dhruv Manilawala
ba98149022 Avoid RUF008 if field annotation is immutable (#4039) 2023-04-20 16:02:12 -04:00
Dhruv Manilawala
7fd44a3e12 Avoid PYI015 for valid default value without annotation (#4043) 2023-04-20 15:45:47 -04:00
Evan Rittenhouse
6e8d561090 Support --fix in watch mode (#4035) 2023-04-19 23:33:12 -04:00
Jacob Coffee
cb762f4cad Add Astral announcement to README (#4010) 2023-04-19 20:28:45 +00:00
569 changed files with 14653 additions and 9412 deletions

View File

@@ -12,3 +12,6 @@ indent_size = 2
[*.{rs,py}]
indent_size = 4
[*.snap]
trim_trailing_whitespace = false

View File

@@ -121,15 +121,6 @@ jobs:
- run: cargo check
- run: cargo fmt --all --check
typos:
name: "spell check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: crate-ci/typos@master
with:
files: .
ecosystem:
name: "ecosystem"
runs-on: ubuntu-latest
@@ -230,3 +221,21 @@ jobs:
exit_code=${PIPESTATUS[0]}
echo '```' >> $GITHUB_STEP_SUMMARY
exit $exit_code
docs:
name: "mkdocs"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v2
- name: "Install dependencies"
run: pip install -r docs/requirements.txt
- name: "Update README File"
run: python scripts/transform_readme.py --target mkdocs
- name: "Generate docs"
run: python scripts/generate_mkdocs.py
- name: "Build docs"
run: mkdocs build --strict

View File

@@ -23,6 +23,11 @@ repos:
- MD033 # no-inline-html
- --
- repo: https://github.com/crate-ci/typos
rev: v1.14.8
hooks:
- id: typos
- repo: local
hooks:
- id: cargo-fmt

View File

@@ -1,5 +1,16 @@
# Breaking Changes
## 0.0.265
### `--fix-only` now exits with a zero exit code, unless `--exit-non-zero-on-fix` is specified ([#4146](https://github.com/charliermarsh/ruff/pull/4146))
Previously, `--fix-only` would exit with a non-zero exit code if any fixes were applied. This
behavior was inconsistent with `--fix`, and further, meant that `--exit-non-zero-on-fix` was
effectively ignored when `--fix-only` was specified.
Now, `--fix-only` will exit with a zero exit code, unless `--exit-non-zero-on-fix` is specified,
in which case it will exit with a non-zero exit code if any fixes were applied.
## 0.0.260
### Fixes are now represented as a list of edits ([#3709](https://github.com/charliermarsh/ruff/pull/3709))

View File

@@ -225,7 +225,7 @@ python scripts/check_ecosystem.py path/to/your/ruff path/to/older/ruff
You can also run the Ecosystem CI check in a Docker container across a larger set of projects by
downloading the [`known-github-tomls.json`](https://github.com/akx/ruff-usage-aggregate/blob/master/data/known-github-tomls.jsonl)
as `github_search.jsonl` and following the instructions in [scripts/Dockerfile.ecosystem](scripts/Dockerfile.ecosystem).
as `github_search.jsonl` and following the instructions in [scripts/Dockerfile.ecosystem](https://github.com/charliermarsh/ruff/blob/main/scripts/Dockerfile.ecosystem).
Note that this check will take a while to run.
## Benchmarks

794
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ members = ["crates/*"]
[workspace.package]
edition = "2021"
rust-version = "1.67"
rust-version = "1.69"
homepage = "https://beta.ruff.rs/docs/"
documentation = "https://beta.ruff.rs/docs/"
repository = "https://github.com/charliermarsh/ruff"
@@ -11,7 +11,7 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
[workspace.dependencies]
anyhow = { version = "1.0.69" }
bitflags = { version = "1.3.2" }
bitflags = { version = "2.1.0" }
chrono = { version = "0.4.23", default-features = false, features = ["clock"] }
clap = { version = "4.1.8", features = ["derive"] }
colored = { version = "2.0.0" }
@@ -30,12 +30,10 @@ path-absolutize = { version = "3.0.14" }
proc-macro2 = { version = "1.0.51" }
quote = { version = "1.0.23" }
regex = { version = "1.7.1" }
ruff_text_size = { git = "https://github.com/charliermarsh/RustPython.git", rev = "c3147d2c1524ebd0e90cf1c2938d770314fd5a5a" }
rustc-hash = { version = "1.1.0" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "c15f670f2c30cfae6b41a1874893590148c74bc4" }
rustpython-parser = { features = [
"lalrpop",
"serde",
], git = "https://github.com/RustPython/RustPython.git", rev = "c15f670f2c30cfae6b41a1874893590148c74bc4" }
rustpython-common = { git = "https://github.com/charliermarsh/RustPython.git", rev = "c3147d2c1524ebd0e90cf1c2938d770314fd5a5a" }
rustpython-parser = { git = "https://github.com/charliermarsh/RustPython.git", rev = "c3147d2c1524ebd0e90cf1c2938d770314fd5a5a" }
schemars = { version = "0.8.12" }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.93", features = ["preserve_order"] }
@@ -44,7 +42,7 @@ similar = { version = "2.2.1" }
smallvec = { version = "1.10.0" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
syn = { version = "1.0.109" }
syn = { version = "2.0.15" }
test-case = { version = "3.0.0" }
textwrap = { version = "0.16.0" }
toml = { version = "0.7.2" }

View File

@@ -55,8 +55,8 @@ Ruff is extremely actively developed and used in major open-source projects like
...and many more.
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster) or
the most recent [project update](https://notes.crmarsh.com/ruff-the-first-200-releases).
Ruff is backed by [Astral](https://astral.sh). Read the [launch post](https://astral.sh/blog/announcing-astral-the-company-behind-ruff),
or the original [project announcement](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
## Testimonials
@@ -137,7 +137,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.262'
rev: 'v0.0.265'
hooks:
- id: ruff
```
@@ -367,9 +367,10 @@ Ruff is used by a number of major open-source projects and companies, including:
- [PDM](https://github.com/pdm-project/pdm)
- [PaddlePaddle](https://github.com/PaddlePaddle/Paddle)
- [Pandas](https://github.com/pandas-dev/pandas)
- [Poetry](https://github.com/python-poetry/poetry)
- [Polars](https://github.com/pola-rs/polars)
- [PostHog](https://github.com/PostHog/posthog)
- Prefect ([Marvin](https://github.com/PrefectHQ/marvin))
- Prefect ([Python SDK](https://github.com/PrefectHQ/prefect), [Marvin](https://github.com/PrefectHQ/marvin))
- [Pydantic](https://github.com/pydantic/pydantic)
- [PyInstaller](https://github.com/pyinstaller/pyinstaller)
- [Pylint](https://github.com/PyCQA/pylint)

View File

@@ -6,3 +6,5 @@ trivias = "trivias"
hel = "hel"
whos = "whos"
spawnve = "spawnve"
ned = "ned"
poit = "poit"

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.262"
version = "0.0.265"
edition = { workspace = true }
rust-version = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.262"
version = "0.0.265"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -17,11 +17,11 @@ name = "ruff"
ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
ruff_macros = { path = "../ruff_macros" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
ruff_python_semantic = { path = "../ruff_python_semantic" }
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
ruff_rustpython = { path = "../ruff_rustpython" }
ruff_text_size = { path = "../ruff_text_size" }
ruff_text_size = { workspace = true }
annotate-snippets = { version = "0.9.1", features = ["color"] }
anyhow = { workspace = true }
@@ -29,7 +29,7 @@ bitflags = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["derive", "string"], optional = true }
colored = { workspace = true }
dirs = { version = "4.0.0" }
dirs = { version = "5.0.0" }
fern = { version = "0.6.1" }
glob = { workspace = true }
globset = { workspace = true }

View File

@@ -14,3 +14,8 @@ def foo(x, y, z):
return False
#import os # noqa: ERA001
class A():
pass
# b = c

View File

@@ -1,9 +1,10 @@
"""
Should emit:
B017 - on lines 20
B017 - on lines 23 and 41
"""
import asyncio
import unittest
import pytest
CONSTANT = True
@@ -34,3 +35,14 @@ class Foobar(unittest.TestCase):
def raises_with_absolute_reference(self):
with self.assertRaises(asyncio.CancelledError):
Foo()
def test_pytest_raises():
with pytest.raises(Exception):
raise ValueError("Hello")
with pytest.raises(Exception, "hello"):
raise ValueError("This is fine")
with pytest.raises(Exception, match="hello"):
raise ValueError("This is also fine")

View File

@@ -172,3 +172,14 @@ def iter_f(names):
if False:
return [lambda: i for i in range(3)] # error
for val in range(3):
def make_func(val=val):
def tmp():
return print(val)
return tmp
funcs.append(make_func())

View File

@@ -0,0 +1,39 @@
"""
Should emit:
B027 - on lines 13, 16, 19, 23
"""
from abc import ABC
class AbstractClass(ABC):
def empty_1(self): # error
...
def empty_2(self): # error
pass
def body_1(self):
print("foo")
...
def body_2(self):
self.body_1()
def foo():
class InnerAbstractClass(ABC):
def empty_1(self): # error
...
def empty_2(self): # error
pass
def body_1(self):
print("foo")
...
def body_2(self):
self.body_1()
return InnerAbstractClass

View File

@@ -149,6 +149,16 @@ for group in groupby(items, key=lambda p: p[1]):
collect_shop_items("Joe", group[1])
# https://github.com/charliermarsh/ruff/issues/4050
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
if _section == "greens":
for item in section_items:
collect_shop_items(shopper, item)
elif _section == "frozen items":
_ = [item for item in section_items]
else:
collect_shop_items(shopper, section_items)
# Make sure we ignore - but don't fail on more complicated invocations
for _key, (_value1, _value2) in groupby(
[("a", (1, 2)), ("b", (3, 4)), ("a", (5, 6))], key=lambda p: p[1]

View File

@@ -21,3 +21,36 @@ def f_c():
def f_ok():
msg = "hello"
raise RuntimeError(msg)
def f_unfixable():
msg = "hello"
raise RuntimeError("This is an example exception")
def f_msg_in_nested_scope():
def nested():
msg = "hello"
raise RuntimeError("This is an example exception")
def f_msg_in_parent_scope():
msg = "hello"
def nested():
raise RuntimeError("This is an example exception")
def f_fix_indentation_check(foo):
if foo:
raise RuntimeError("This is an example exception")
else:
if foo == "foo":
raise RuntimeError(f"This is an exception: {foo}")
raise RuntimeError("This is an exception: {}".format(foo))
# Report these, but don't fix them
if foo: raise RuntimeError("This is an example exception")
if foo: x = 1; raise RuntimeError("This is an example exception")

View File

@@ -0,0 +1,10 @@
from logging.config import BaseConfigurator # banned
from typing import Any, Dict # banned
from typing import * # banned
from pandas import DataFrame # banned
from pandas import * # banned
import logging.config # ok
import typing # ok
import pandas # ok

View File

@@ -84,3 +84,10 @@ class Class1:
# We shouldn't emit Y015 for __all__
__all__ = ["Class1"]
# Ignore the following for PYI015
field26 = typing.Sequence[int]
field27 = list[str]
field28 = builtins.str
field29 = str
field30 = str | bytes | None

View File

@@ -91,3 +91,10 @@ class Class1:
# We shouldn't emit Y015 for __all__
__all__ = ["Class1"]
# Ignore the following for PYI015
field26 = typing.Sequence[int]
field27 = list[str]
field28 = builtins.str
field29 = str
field30 = str | bytes | None

View File

@@ -0,0 +1,28 @@
import sys
import typing
from typing import Annotated, Literal, TypeAlias, TypeVar
import typing_extensions
def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
_T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs
Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs
class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs
"""Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs
if sys.platform == "linux":
f: "int" # Y020 Quoted annotations should never be used in stubs
elif sys.platform == "win32":
f: "str" # Y020 Quoted annotations should never be used in stubs
else:
f: "bytes" # Y020 Quoted annotations should never be used in stubs
# These two shouldn't trigger Y020 -- empty strings can't be "quoted annotations"
k = "" # Y052 Need type annotation for "k"
el = r"" # Y052 Need type annotation for "el"

View File

@@ -0,0 +1,28 @@
import sys
import typing
from typing import Annotated, Literal, TypeAlias, TypeVar
import typing_extensions
def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
_T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs
Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs
class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs
"""Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs
if sys.platform == "linux":
f: "int" # Y020 Quoted annotations should never be used in stubs
elif sys.platform == "win32":
f: "str" # Y020 Quoted annotations should never be used in stubs
else:
f: "bytes" # Y020 Quoted annotations should never be used in stubs
# These two shouldn't trigger Y020 -- empty strings can't be "quoted annotations"
k = "" # Y052 Need type annotation for "k"
el = r"" # Y052 Need type annotation for "el"

View File

@@ -0,0 +1,24 @@
import typing
from collections.abc import Mapping
from typing import (
Annotated,
TypeAlias,
Union,
Literal,
)
just_literals_pipe_union: TypeAlias = (
Literal[True] | Literal["idk"]
) # not PYI042 (not a stubfile)
PublicAliasT: TypeAlias = str | int
PublicAliasT2: TypeAlias = Union[str, bytes]
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
snake_case_alias1: TypeAlias = str | int # not PYI042 (not a stubfile)
_snake_case_alias2: TypeAlias = Literal["whatever"] # not PYI042 (not a stubfile)
Snake_case_alias: TypeAlias = int | float # not PYI042 (not a stubfile)
# check that this edge case doesn't crash
_: TypeAlias = str | int

View File

@@ -0,0 +1,24 @@
import typing
from collections.abc import Mapping
from typing import (
Annotated,
TypeAlias,
Union,
Literal,
)
just_literals_pipe_union: TypeAlias = (
Literal[True] | Literal["idk"]
) # PYI042, since not camel case
PublicAliasT: TypeAlias = str | int
PublicAliasT2: TypeAlias = Union[str, bytes]
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
snake_case_alias1: TypeAlias = str | int # PYI042, since not camel case
_snake_case_alias2: TypeAlias = Literal["whatever"] # PYI042, since not camel case
Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
# check that this edge case doesn't crash
_: TypeAlias = str | int

View File

@@ -0,0 +1,23 @@
import typing
from collections.abc import Mapping
from typing import (
Annotated,
TypeAlias,
Union,
Literal,
)
_PrivateAliasT: TypeAlias = str | int # not PYI043 (not a stubfile)
_PrivateAliasT2: TypeAlias = typing.Any # not PYI043 (not a stubfile)
_PrivateAliasT3: TypeAlias = Literal[
"not", "a", "chance"
] # not PYI043 (not a stubfile)
just_literals_pipe_union: TypeAlias = Literal[True] | Literal["idk"]
PublicAliasT: TypeAlias = str | int
PublicAliasT2: TypeAlias = Union[str, bytes]
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
# check that this edge case doesn't crash
_: TypeAlias = str | int

View File

@@ -0,0 +1,23 @@
import typing
from collections.abc import Mapping
from typing import (
Annotated,
TypeAlias,
Union,
Literal,
)
_PrivateAliasT: TypeAlias = str | int # PYI043, since this ends in a T
_PrivateAliasT2: TypeAlias = typing.Any # PYI043, since this ends in a T
_PrivateAliasT3: TypeAlias = Literal[
"not", "a", "chance"
] # PYI043, since this ends in a T
just_literals_pipe_union: TypeAlias = Literal[True] | Literal["idk"]
PublicAliasT: TypeAlias = str | int
PublicAliasT2: TypeAlias = Union[str, bytes]
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
# check that this edge case doesn't crash
_: TypeAlias = str | int

View File

@@ -0,0 +1,7 @@
"""Case: There's a random import, so it should add `contextlib` after it."""
import math
try:
math.sqrt(-1)
except ValueError: # SIM105
pass

View File

@@ -0,0 +1,12 @@
"""Case: `contextlib` already imported."""
import contextlib
def foo():
pass
try:
foo()
except ValueError:
pass

View File

@@ -42,3 +42,113 @@ if False and f() and a and g() and b: # OK
if a and False and f() and b and g(): # OK
pass
a or "" or True # SIM222
a or "foo" or True or "bar" # SIM222
a or 0 or True # SIM222
a or 1 or True or 2 # SIM222
a or 0.0 or True # SIM222
a or 0.1 or True or 0.2 # SIM222
a or [] or True # SIM222
a or list([]) or True # SIM222
a or [1] or True or [2] # SIM222
a or list([1]) or True or list([2]) # SIM222
a or {} or True # SIM222
a or dict() or True # SIM222
a or {1: 1} or True or {2: 2} # SIM222
a or dict({1: 1}) or True or dict({2: 2}) # SIM222
a or set() or True # SIM222
a or set(set()) or True # SIM222
a or {1} or True or {2} # SIM222
a or set({1}) or True or set({2}) # SIM222
a or () or True # SIM222
a or tuple(()) or True # SIM222
a or (1,) or True or (2,) # SIM222
a or tuple((1,)) or True or tuple((2,)) # SIM222
a or frozenset() or True # SIM222
a or frozenset(frozenset()) or True # SIM222
a or frozenset({1}) or True or frozenset({2}) # SIM222
a or frozenset(frozenset({1})) or True or frozenset(frozenset({2})) # SIM222
# Inside test `a` is simplified.
bool(a or [1] or True or [2]) # SIM222
assert a or [1] or True or [2] # SIM222
if (a or [1] or True or [2]) and (a or [1] or True or [2]): # SIM222
pass
0 if a or [1] or True or [2] else 1 # SIM222
while a or [1] or True or [2]: # SIM222
pass
[
0
for a in range(10)
for b in range(10)
if a or [1] or True or [2] # SIM222
if b or [1] or True or [2] # SIM222
]
{
0
for a in range(10)
for b in range(10)
if a or [1] or True or [2] # SIM222
if b or [1] or True or [2] # SIM222
}
{
0: 0
for a in range(10)
for b in range(10)
if a or [1] or True or [2] # SIM222
if b or [1] or True or [2] # SIM222
}
(
0
for a in range(10)
for b in range(10)
if a or [1] or True or [2] # SIM222
if b or [1] or True or [2] # SIM222
)
# Outside test `a` is not simplified.
a or [1] or True or [2] # SIM222
if (a or [1] or True or [2]) == (a or [1]): # SIM222
pass
if f(a or [1] or True or [2]): # SIM222
pass

View File

@@ -37,3 +37,113 @@ if True or f() or a or g() or b: # OK
if a or True or f() or b or g(): # OK
pass
a and "" and False # SIM223
a and "foo" and False and "bar" # SIM223
a and 0 and False # SIM223
a and 1 and False and 2 # SIM223
a and 0.0 and False # SIM223
a and 0.1 and False and 0.2 # SIM223
a and [] and False # SIM223
a and list([]) and False # SIM223
a and [1] and False and [2] # SIM223
a and list([1]) and False and list([2]) # SIM223
a and {} and False # SIM223
a and dict() and False # SIM223
a and {1: 1} and False and {2: 2} # SIM223
a and dict({1: 1}) and False and dict({2: 2}) # SIM223
a and set() and False # SIM223
a and set(set()) and False # SIM223
a and {1} and False and {2} # SIM223
a and set({1}) and False and set({2}) # SIM223
a and () and False # SIM222
a and tuple(()) and False # SIM222
a and (1,) and False and (2,) # SIM222
a and tuple((1,)) and False and tuple((2,)) # SIM222
a and frozenset() and False # SIM222
a and frozenset(frozenset()) and False # SIM222
a and frozenset({1}) and False and frozenset({2}) # SIM222
a and frozenset(frozenset({1})) and False and frozenset(frozenset({2})) # SIM222
# Inside test `a` is simplified.
bool(a and [] and False and []) # SIM223
assert a and [] and False and [] # SIM223
if (a and [] and False and []) or (a and [] and False and []): # SIM223
pass
0 if a and [] and False and [] else 1 # SIM222
while a and [] and False and []: # SIM223
pass
[
0
for a in range(10)
for b in range(10)
if a and [] and False and [] # SIM223
if b and [] and False and [] # SIM223
]
{
0
for a in range(10)
for b in range(10)
if a and [] and False and [] # SIM223
if b and [] and False and [] # SIM223
}
{
0: 0
for a in range(10)
for b in range(10)
if a and [] and False and [] # SIM223
if b and [] and False and [] # SIM223
}
(
0
for a in range(10)
for b in range(10)
if a and [] and False and [] # SIM223
if b and [] and False and [] # SIM223
)
# Outside test `a` is not simplified.
a and [] and False and [] # SIM223
if (a and [] and False and []) == (a and []): # SIM223
pass
if f(a and [] and False and []): # SIM223
pass

View File

@@ -22,3 +22,6 @@ from bar import (
a, # comment 7
b, # comment 8
)
# comment 9
from baz import * # comment 10

View File

@@ -0,0 +1,4 @@
from mypackage.subpackage import ( # long comment that seems to be a problem
a_long_variable_name_that_causes_problems,
items,
)

View File

@@ -13,3 +13,11 @@ class C:
myObj2 = namedtuple("MyObj2", ["a", "b"])
Employee = NamedTuple('Employee', [('name', str), ('id', int)])
Point2D = TypedDict('Point2D', {'in': int, 'x-y': int})
class D(TypedDict):
lower: int
CONSTANT: str
mixedCase: bool
_mixedCase: list
mixed_Case: set

View File

@@ -4,11 +4,11 @@ if not X is Y:
#: E714
if not X.B is Y:
pass
#: E714
#: Okay
if not X is Y is not Z:
pass
#: Okay
if not X is not Y:
pass

View File

@@ -9,6 +9,9 @@ def f():
# Here's a standalone comment that's over the limit.
x = 2
# Another standalone that is preceded by a newline and indent toke and is over the limit.
print("Here's a string that's over the limit, but it's not a docstring.")

View File

@@ -0,0 +1,21 @@
"""A module docstring with D214 violations
Returns
-----
valid returns
Args
-----
valid args
"""
import os
from .expected import Expectation
expectation = Expectation()
expect = expectation.expect
expect(os.path.normcase(__file__ if __file__[-1] != 'c' else __file__[:-1]),
"D214: Section is over-indented ('Returns')")
expect(os.path.normcase(__file__ if __file__[-1] != 'c' else __file__[:-1]),
"D214: Section is over-indented ('Args')")

View File

@@ -115,6 +115,20 @@ def f(x, *args, **kwargs):
return x
def f(x, *, y, z):
"""Do something.
Args:
x: some first value
Keyword Args:
y (int): the other value
z (int): the last value
"""
return x, y, z
class Test:
def f(self, /, arg1: int) -> None:
"""

View File

@@ -0,0 +1,13 @@
def redef(value):
match value:
case True:
def fun(x, y):
return x
case False:
def fun(x, y):
return y
return fun

View File

@@ -132,3 +132,8 @@ def in_ipython_notebook() -> bool:
except NameError:
return False # not in notebook
return True
def named_expr():
if any((key := (value := x)) for x in ["ok"]):
print(key)

View File

@@ -121,3 +121,8 @@ def f(x: int):
print("A")
case y:
pass
def f():
if any((key := (value := x)) for x in ["ok"]):
print(key)

View File

@@ -0,0 +1,3 @@
import import_self.module
from import_self import module
from . import module

View File

@@ -0,0 +1,51 @@
class TestClass:
def __bool__(self):
...
def __bool__(self, x): # too many mandatory args
...
def __bool__(self, x=1): # additional optional args OK
...
def __bool__(self, *args): # varargs OK
...
def __bool__(): # ignored; should be caughty by E0211/N805
...
@staticmethod
def __bool__():
...
@staticmethod
def __bool__(x): # too many mandatory args
...
@staticmethod
def __bool__(x=1): # additional optional args OK
...
def __eq__(self, other): # multiple args
...
def __eq__(self, other=1): # expected arg is optional
...
def __eq__(self): # too few mandatory args
...
def __eq__(self, other, other_other): # too many mandatory args
...
def __round__(self): # allow zero additional args.
...
def __round__(self, x): # allow one additional args.
...
def __round__(self, x, y): # disallow 2 args
...
def __round__(self, x, y, z=2): # disallow 3 args even when one is optional
...

View File

@@ -46,6 +46,14 @@ print("foo {} ".format(x))
'({}={{0!e}})'.format(a)
"{[b]}".format(a)
'{[b]}'.format(a)
"""{[b]}""".format(a)
'''{[b]}'''.format(a)
###
# Non-errors
###

View File

@@ -1,4 +1,6 @@
import typing
from dataclasses import dataclass, field
from typing import ClassVar, Sequence
KNOWINGLY_MUTABLE_DEFAULT = []
@@ -6,16 +8,20 @@ KNOWINGLY_MUTABLE_DEFAULT = []
@dataclass()
class A:
mutable_default: list[int] = []
immutable_annotation: typing.Sequence[int] = []
without_annotation = []
ignored_via_comment: list[int] = [] # noqa: RUF008
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: typing.ClassVar[list[int]] = []
@dataclass
class B:
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
ignored_via_comment: list[int] = [] # noqa: RUF008
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []

View File

@@ -1,5 +1,9 @@
from dataclasses import dataclass
from typing import NamedTuple
import datetime
import re
import typing
from dataclasses import dataclass, field
from pathlib import Path
from typing import ClassVar, NamedTuple
def default_function() -> list[int]:
@@ -13,6 +17,14 @@ class ImmutableType(NamedTuple):
@dataclass()
class A:
hidden_mutable_default: list[int] = default_function()
class_variable: typing.ClassVar[list[int]] = default_function()
another_class_var: ClassVar[list[int]] = default_function()
fine_path: Path = Path()
fine_date: datetime.date = datetime.date(2042, 1, 1)
fine_timedelta: datetime.timedelta = datetime.timedelta(hours=7)
fine_tuple: tuple[int] = tuple([1])
fine_regex: re.Pattern = re.compile(r".*")
DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40)
@@ -26,3 +38,5 @@ class B:
not_optimal: ImmutableType = ImmutableType(20)
good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
fine_dataclass_function: list[int] = field(default_factory=list)

View File

@@ -4,12 +4,12 @@ use itertools::Itertools;
use libcst_native::{
Codegen, CodegenState, ImportNames, ParenthesizableWhitespace, SmallStatement, Statement,
};
use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Location, Stmt, StmtKind};
use ruff_text_size::{TextLen, TextRange, TextSize};
use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Stmt, StmtKind};
use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::Edit;
use ruff_python_ast::helpers;
use ruff_python_ast::helpers::to_absolute;
use ruff_python_ast::imports::{AnyImport, Import};
use ruff_python_ast::newlines::NewlineWithTrailingNewline;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
@@ -102,20 +102,17 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool>
/// Return the location of a trailing semicolon following a `Stmt`, if it's part
/// of a multi-statement line.
fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
let contents = locator.after(stmt.end_location.unwrap());
for (row, line) in NewlineWithTrailingNewline::from(contents).enumerate() {
let trimmed = line.trim();
fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<TextSize> {
let contents = locator.after(stmt.end());
for line in NewlineWithTrailingNewline::from(contents) {
let trimmed = line.trim_start();
if trimmed.starts_with(';') {
let column = line
.char_indices()
.find_map(|(column, char)| if char == ';' { Some(column) } else { None })
.unwrap();
return Some(to_absolute(
Location::new(row + 1, column),
stmt.end_location.unwrap(),
));
let colon_offset = line.text_len() - trimmed.text_len();
return Some(stmt.end() + line.start() + colon_offset);
}
if !trimmed.starts_with('\\') {
break;
}
@@ -124,42 +121,36 @@ fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
}
/// Find the next valid break for a `Stmt` after a semicolon.
fn next_stmt_break(semicolon: Location, locator: &Locator) -> Location {
let start_location = Location::new(semicolon.row(), semicolon.column() + 1);
let contents = locator.after(start_location);
for (row, line) in NewlineWithTrailingNewline::from(contents).enumerate() {
fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
let start_location = semicolon + TextSize::from(1);
let contents = &locator.contents()[usize::from(start_location)..];
for line in NewlineWithTrailingNewline::from(contents) {
let trimmed = line.trim();
// Skip past any continuations.
if trimmed.starts_with('\\') {
continue;
}
return if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
to_absolute(Location::new(row + 1, 0), start_location)
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let column = line
.char_indices()
.find_map(|(column, char)| {
if char.is_whitespace() {
None
} else {
Some(column)
}
})
.unwrap();
to_absolute(Location::new(row + 1, column), start_location)
};
return start_location
+ if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
line.start()
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let relative_offset = line.find(|c: char| !c.is_whitespace()).unwrap();
line.start() + TextSize::try_from(relative_offset).unwrap()
};
}
Location::new(start_location.row() + 1, 0)
locator.line_end(start_location)
}
/// Return `true` if a `Stmt` occurs at the end of a file.
fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool {
let contents = locator.after(stmt.end_location.unwrap());
contents.is_empty()
stmt.end() == locator.contents().text_len()
}
/// Return the `Fix` to use when deleting a `Stmt`.
@@ -190,33 +181,23 @@ pub fn delete_stmt(
{
// If removing this node would lead to an invalid syntax tree, replace
// it with a `pass`.
Ok(Edit::replacement(
"pass".to_string(),
stmt.location,
stmt.end_location.unwrap(),
))
Ok(Edit::range_replacement("pass".to_string(), stmt.range()))
} else {
Ok(if let Some(semicolon) = trailing_semicolon(stmt, locator) {
let next = next_stmt_break(semicolon, locator);
Edit::deletion(stmt.location, next)
} else if helpers::match_leading_content(stmt, locator) {
Edit::deletion(stmt.location, stmt.end_location.unwrap())
} else if helpers::preceded_by_continuation(stmt, indexer) {
if is_end_of_file(stmt, locator) && stmt.location.column() == 0 {
Edit::deletion(stmt.start(), next)
} else if helpers::has_leading_content(stmt, locator) {
Edit::range_deletion(stmt.range())
} else if helpers::preceded_by_continuation(stmt, indexer, locator) {
if is_end_of_file(stmt, locator) && locator.is_at_start_of_line(stmt.start()) {
// Special-case: a file can't end in a continuation.
Edit::replacement(
stylist.line_ending().to_string(),
stmt.location,
stmt.end_location.unwrap(),
)
Edit::range_replacement(stylist.line_ending().to_string(), stmt.range())
} else {
Edit::deletion(stmt.location, stmt.end_location.unwrap())
Edit::range_deletion(stmt.range())
}
} else {
Edit::deletion(
Location::new(stmt.location.row(), 0),
Location::new(stmt.end_location.unwrap().row() + 1, 0),
)
let range = locator.full_lines_range(stmt.range());
Edit::range_deletion(range)
})
}
}
@@ -231,7 +212,7 @@ pub fn remove_unused_imports<'a>(
indexer: &Indexer,
stylist: &Stylist,
) -> Result<Edit> {
let module_text = locator.slice(stmt);
let module_text = locator.slice(stmt.range());
let mut tree = match_module(module_text)?;
let Some(Statement::Simple(body)) = tree.body.first_mut() else {
@@ -337,11 +318,7 @@ pub fn remove_unused_imports<'a>(
};
tree.codegen(&mut state);
Ok(Edit::replacement(
state.to_string(),
stmt.location,
stmt.end_location.unwrap(),
))
Ok(Edit::range_replacement(state.to_string(), stmt.range()))
}
}
@@ -353,9 +330,8 @@ pub fn remove_unused_imports<'a>(
/// For this behavior, set `remove_parentheses` to `true`.
pub fn remove_argument(
locator: &Locator,
call_at: Location,
expr_at: Location,
expr_end: Location,
call_at: TextSize,
expr_range: TextRange,
args: &[Expr],
keywords: &[Keyword],
remove_parentheses: bool,
@@ -374,13 +350,13 @@ pub fn remove_argument(
if n_arguments == 1 {
// Case 1: there is only one argument.
let mut count: usize = 0;
for (start, tok, end) in lexer::lex_located(contents, Mode::Module, call_at).flatten() {
for (tok, range) in lexer::lex_located(contents, Mode::Module, call_at).flatten() {
if matches!(tok, Tok::Lpar) {
if count == 0 {
fix_start = Some(if remove_parentheses {
start
range.start()
} else {
Location::new(start.row(), start.column() + 1)
range.start() + TextSize::from(1)
});
}
count += 1;
@@ -390,9 +366,9 @@ pub fn remove_argument(
count -= 1;
if count == 0 {
fix_end = Some(if remove_parentheses {
end
range.end()
} else {
Location::new(end.row(), end.column() - 1)
range.end() - TextSize::from(1)
});
break;
}
@@ -400,27 +376,27 @@ pub fn remove_argument(
}
} else if args
.iter()
.map(|node| node.location)
.chain(keywords.iter().map(|node| node.location))
.any(|location| location > expr_at)
.map(Expr::start)
.chain(keywords.iter().map(Keyword::start))
.any(|location| location > expr_range.start())
{
// Case 2: argument or keyword is _not_ the last node.
let mut seen_comma = false;
for (start, tok, end) in lexer::lex_located(contents, Mode::Module, call_at).flatten() {
for (tok, range) in lexer::lex_located(contents, Mode::Module, call_at).flatten() {
if seen_comma {
if matches!(tok, Tok::NonLogicalNewline) {
// Also delete any non-logical newlines after the comma.
continue;
}
fix_end = Some(if matches!(tok, Tok::Newline) {
end
range.end()
} else {
start
range.start()
});
break;
}
if start == expr_at {
fix_start = Some(start);
if range.start() == expr_range.start() {
fix_start = Some(range.start());
}
if fix_start.is_some() && matches!(tok, Tok::Comma) {
seen_comma = true;
@@ -429,13 +405,13 @@ pub fn remove_argument(
} else {
// Case 3: argument or keyword is the last node, so we have to find the last
// comma in the stmt.
for (start, tok, _) in lexer::lex_located(contents, Mode::Module, call_at).flatten() {
if start == expr_at {
fix_end = Some(expr_end);
for (tok, range) in lexer::lex_located(contents, Mode::Module, call_at).flatten() {
if range.start() == expr_range.start() {
fix_end = Some(expr_range.end());
break;
}
if matches!(tok, Tok::Comma) {
fix_start = Some(start);
fix_start = Some(range.start());
}
}
}
@@ -482,11 +458,8 @@ pub fn get_or_import_symbol(
//
// By adding this no-op edit, we force the `unused-imports` fix to conflict with the
// `sys-exit-alias` fix, and thus will avoid applying both fixes in the same pass.
let import_edit = Edit::replacement(
locator.slice(source).to_string(),
source.location,
source.end_location.unwrap(),
);
let import_edit =
Edit::range_replacement(locator.slice(source.range()).to_string(), source.range());
Ok((import_edit, binding))
} else {
if let Some(stmt) = importer.get_import_from(module) {
@@ -527,8 +500,8 @@ pub fn get_or_import_symbol(
#[cfg(test)]
mod tests {
use anyhow::Result;
use ruff_text_size::TextSize;
use rustpython_parser as parser;
use rustpython_parser::ast::Location;
use ruff_python_ast::source_code::Locator;
@@ -546,19 +519,13 @@ mod tests {
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(
trailing_semicolon(stmt, &locator),
Some(Location::new(1, 5))
);
assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(5)));
let contents = "x = 1 ; y = 1";
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(
trailing_semicolon(stmt, &locator),
Some(Location::new(1, 6))
);
assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(6)));
let contents = r#"
x = 1 \
@@ -568,10 +535,7 @@ x = 1 \
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(
trailing_semicolon(stmt, &locator),
Some(Location::new(2, 2))
);
assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(10)));
Ok(())
}
@@ -581,15 +545,15 @@ x = 1 \
let contents = "x = 1; y = 1";
let locator = Locator::new(contents);
assert_eq!(
next_stmt_break(Location::new(1, 4), &locator),
Location::new(1, 5)
next_stmt_break(TextSize::from(4), &locator),
TextSize::from(5)
);
let contents = "x = 1 ; y = 1";
let locator = Locator::new(contents);
assert_eq!(
next_stmt_break(Location::new(1, 5), &locator),
Location::new(1, 6)
next_stmt_break(TextSize::from(5), &locator),
TextSize::from(6)
);
let contents = r#"
@@ -599,8 +563,8 @@ x = 1 \
.trim();
let locator = Locator::new(contents);
assert_eq!(
next_stmt_break(Location::new(2, 2), &locator),
Location::new(2, 4)
next_stmt_break(TextSize::from(10), &locator),
TextSize::from(12)
);
}
}

View File

@@ -1,12 +1,11 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use rustpython_parser::ast::Location;
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::types::Range;
use crate::linter::FixTable;
use crate::registry::{AsRule, Rule};
@@ -33,7 +32,7 @@ fn apply_fixes<'a>(
locator: &'a Locator<'a>,
) -> (String, FixTable) {
let mut output = String::with_capacity(locator.len());
let mut last_pos: Option<Location> = None;
let mut last_pos: Option<TextSize> = None;
let mut applied: BTreeSet<&Edit> = BTreeSet::default();
let mut fixed = FxHashMap::default();
@@ -57,7 +56,7 @@ fn apply_fixes<'a>(
// Best-effort approach: if this fix overlaps with a fix we've already applied,
// skip it.
if last_pos.map_or(false, |last_pos| {
fix.min_location()
fix.min_start()
.map_or(false, |fix_location| last_pos >= fix_location)
}) {
continue;
@@ -65,14 +64,14 @@ fn apply_fixes<'a>(
for edit in fix.edits() {
// Add all contents from `last_pos` to `fix.location`.
let slice = locator.slice(Range::new(last_pos.unwrap_or_default(), edit.location()));
let slice = locator.slice(TextRange::new(last_pos.unwrap_or_default(), edit.start()));
output.push_str(slice);
// Add the patch itself.
output.push_str(edit.content().unwrap_or_default());
// Track that the edit was applied.
last_pos = Some(edit.end_location());
last_pos = Some(edit.end());
applied.insert(edit);
}
@@ -88,8 +87,8 @@ fn apply_fixes<'a>(
/// Compare two fixes.
fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Ordering {
fix1.min_location()
.cmp(&fix2.min_location())
fix1.min_start()
.cmp(&fix2.min_start())
.then_with(|| match (&rule1, &rule2) {
// Apply `EndsInPeriod` fixes before `NewLineAfterLastParagraph` fixes.
(Rule::EndsInPeriod, Rule::NewLineAfterLastParagraph) => std::cmp::Ordering::Less,
@@ -100,7 +99,7 @@ fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Orderi
#[cfg(test)]
mod tests {
use rustpython_parser::ast::Location;
use ruff_text_size::TextSize;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Edit;
@@ -114,8 +113,7 @@ mod tests {
.map(|edit| Diagnostic {
// The choice of rule here is arbitrary.
kind: MissingNewlineAtEndOfFile.into(),
location: edit.location(),
end_location: edit.end_location(),
range: edit.range(),
fix: edit.into(),
parent: None,
})
@@ -142,8 +140,8 @@ class A(object):
);
let diagnostics = create_diagnostics([Edit::replacement(
"Bar".to_string(),
Location::new(1, 8),
Location::new(1, 14),
TextSize::new(8),
TextSize::new(14),
)]);
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(
@@ -166,8 +164,7 @@ class A(object):
"#
.trim(),
);
let diagnostics =
create_diagnostics([Edit::deletion(Location::new(1, 7), Location::new(1, 15))]);
let diagnostics = create_diagnostics([Edit::deletion(TextSize::new(7), TextSize::new(15))]);
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(
contents,
@@ -190,8 +187,8 @@ class A(object, object, object):
.trim(),
);
let diagnostics = create_diagnostics([
Edit::deletion(Location::new(1, 8), Location::new(1, 16)),
Edit::deletion(Location::new(1, 22), Location::new(1, 30)),
Edit::deletion(TextSize::from(8), TextSize::from(16)),
Edit::deletion(TextSize::from(22), TextSize::from(30)),
]);
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
@@ -216,12 +213,8 @@ class A(object):
.trim(),
);
let diagnostics = create_diagnostics([
Edit::deletion(Location::new(1, 7), Location::new(1, 15)),
Edit::replacement(
"ignored".to_string(),
Location::new(1, 9),
Location::new(1, 11),
),
Edit::deletion(TextSize::from(7), TextSize::from(15)),
Edit::replacement("ignored".to_string(), TextSize::from(9), TextSize::from(11)),
]);
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(

View File

@@ -1,14 +1,14 @@
use ruff_python_semantic::scope::ScopeStack;
use ruff_text_size::TextRange;
use rustpython_parser::ast::{Expr, Stmt};
use ruff_python_ast::types::Range;
use ruff_python_ast::types::RefEquality;
use ruff_python_semantic::analyze::visibility::{Visibility, VisibleScope};
use ruff_python_semantic::scope::ScopeId;
use crate::checkers::ast::AnnotationContext;
use crate::docstrings::definition::Definition;
type Context<'a> = (ScopeStack, Vec<RefEquality<'a, Stmt>>);
type Context<'a> = (ScopeId, Vec<RefEquality<'a, Stmt>>);
/// A collection of AST nodes that are deferred for later analysis.
/// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all
@@ -16,7 +16,7 @@ type Context<'a> = (ScopeStack, Vec<RefEquality<'a, Stmt>>);
#[derive(Default)]
pub struct Deferred<'a> {
pub definitions: Vec<(Definition<'a>, Visibility, Context<'a>)>,
pub string_type_definitions: Vec<(Range, &'a str, AnnotationContext, Context<'a>)>,
pub string_type_definitions: Vec<(TextRange, &'a str, AnnotationContext, Context<'a>)>,
pub type_definitions: Vec<(&'a Expr, AnnotationContext, Context<'a>)>,
pub functions: Vec<(&'a Stmt, Context<'a>, VisibleScope)>,
pub lambdas: Vec<(&'a Expr, Context<'a>)>,

File diff suppressed because it is too large Load Diff

View File

@@ -30,13 +30,11 @@ fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) ->
for stmt in blocks.iter().flat_map(|block| &block.imports) {
match &stmt.node {
StmtKind::Import { names } => {
module_imports.extend(names.iter().map(|name| {
ModuleImport::new(
name.node.name.clone(),
stmt.location,
stmt.end_location.unwrap(),
)
}));
module_imports.extend(
names
.iter()
.map(|name| ModuleImport::new(name.node.name.clone(), stmt.range())),
);
}
StmtKind::ImportFrom {
module,
@@ -61,11 +59,7 @@ fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) ->
Cow::Owned(module_path[..module_path.len() - level].join("."))
};
module_imports.extend(names.iter().map(|name| {
ModuleImport::new(
format!("{}.{}", module, name.node.name),
name.location,
name.end_location.unwrap(),
)
ModuleImport::new(format!("{}.{}", module, name.node.name), name.range())
}));
}
_ => panic!("Expected StmtKind::Import | StmtKind::ImportFrom"),

View File

@@ -1,9 +1,9 @@
use rustpython_parser::ast::Location;
use ruff_text_size::TextRange;
use rustpython_parser::lexer::LexResult;
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_python_ast::source_code::{Locator, Stylist};
use ruff_python_ast::types::Range;
use ruff_python_ast::token_kind::TokenKind;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle::rules::logical_lines::{
@@ -37,7 +37,7 @@ pub fn check_logical_lines(
settings: &Settings,
autofix: flags::Autofix,
) -> Vec<Diagnostic> {
let mut diagnostics = vec![];
let mut context = LogicalLinesContext::new(settings);
#[cfg(feature = "logical_lines")]
let should_fix_missing_whitespace =
@@ -59,120 +59,51 @@ pub fn check_logical_lines(
for line in &LogicalLines::from_tokens(tokens, locator) {
if line.flags().contains(TokenFlags::OPERATOR) {
for (location, kind) in space_around_operator(&line) {
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location,
end_location: location,
fix: Fix::empty(),
parent: None,
});
}
}
for (location, kind) in whitespace_around_named_parameter_equals(&line.tokens()) {
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location,
end_location: location,
fix: Fix::empty(),
parent: None,
});
}
}
for (location, kind) in missing_whitespace_around_operator(&line.tokens()) {
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location,
end_location: location,
fix: Fix::empty(),
parent: None,
});
}
}
for diagnostic in missing_whitespace(&line, should_fix_missing_whitespace) {
if settings.rules.enabled(diagnostic.kind.rule()) {
diagnostics.push(diagnostic);
}
}
space_around_operator(&line, &mut context);
whitespace_around_named_parameter_equals(&line, &mut context);
missing_whitespace_around_operator(&line, &mut context);
missing_whitespace(&line, should_fix_missing_whitespace, &mut context);
}
if line
.flags()
.contains(TokenFlags::OPERATOR | TokenFlags::PUNCTUATION)
{
for (location, kind) in extraneous_whitespace(&line) {
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location,
end_location: location,
fix: Fix::empty(),
parent: None,
});
}
}
extraneous_whitespace(&line, &mut context);
}
if line.flags().contains(TokenFlags::KEYWORD) {
for (location, kind) in whitespace_around_keywords(&line) {
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location,
end_location: location,
fix: Fix::empty(),
parent: None,
});
}
}
for (location, kind) in missing_whitespace_after_keyword(&line.tokens()) {
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location,
end_location: location,
fix: Fix::empty(),
parent: None,
});
}
}
whitespace_around_keywords(&line, &mut context);
missing_whitespace_after_keyword(&line, &mut context);
}
if line.flags().contains(TokenFlags::COMMENT) {
for (range, kind) in whitespace_before_comment(&line.tokens(), locator) {
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location: range.location,
end_location: range.end_location,
fix: Fix::empty(),
parent: None,
});
}
}
whitespace_before_comment(&line, locator, prev_line.is_none(), &mut context);
}
if line.flags().contains(TokenFlags::BRACKET) {
for diagnostic in whitespace_before_parameters(
&line.tokens(),
whitespace_before_parameters(
&line,
should_fix_whitespace_before_parameters,
) {
if settings.rules.enabled(diagnostic.kind.rule()) {
diagnostics.push(diagnostic);
}
}
&mut context,
);
}
// Extract the indentation level.
let Some(start_loc) = line.first_token_location() else { continue; };
let start_line = locator.slice(Range::new(Location::new(start_loc.row(), 0), start_loc));
let indent_level = expand_indent(start_line);
let Some(first_token) = line.first_token() else {
continue;
};
let range = if first_token.kind() == TokenKind::Indent {
first_token.range()
} else {
TextRange::new(locator.line_start(first_token.start()), first_token.start())
};
let indent_level = expand_indent(locator.slice(range));
let indent_size = 4;
for (location, kind) in indentation(
for kind in indentation(
&line,
prev_line.as_ref(),
indent_char,
@@ -181,13 +112,7 @@ pub fn check_logical_lines(
indent_size,
) {
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location: Location::new(start_loc.row(), 0),
end_location: location,
fix: Fix::empty(),
parent: None,
});
context.push(kind, range);
}
}
@@ -196,7 +121,40 @@ pub fn check_logical_lines(
prev_indent_level = Some(indent_level);
}
}
diagnostics
context.diagnostics
}
#[derive(Debug, Clone)]
pub(crate) struct LogicalLinesContext<'a> {
settings: &'a Settings,
diagnostics: Vec<Diagnostic>,
}
impl<'a> LogicalLinesContext<'a> {
fn new(settings: &'a Settings) -> Self {
Self {
settings,
diagnostics: Vec::new(),
}
}
pub fn push<K: Into<DiagnosticKind>>(&mut self, kind: K, range: TextRange) {
let kind = kind.into();
if self.settings.rules.enabled(kind.rule()) {
self.diagnostics.push(Diagnostic {
kind,
range,
fix: Fix::empty(),
parent: None,
});
}
}
pub fn push_diagnostic(&mut self, diagnostic: Diagnostic) {
if self.settings.rules.enabled(diagnostic.kind.rule()) {
self.diagnostics.push(diagnostic);
}
}
}
#[cfg(test)]

View File

@@ -1,15 +1,13 @@
//! `NoQA` enforcement and validation.
use nohash_hasher::IntMap;
use rustpython_parser::ast::Location;
use itertools::Itertools;
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, Edit};
use ruff_python_ast::newlines::StrExt;
use ruff_python_ast::types::Range;
use ruff_python_ast::source_code::Locator;
use crate::codes::NoqaCode;
use crate::noqa;
use crate::noqa::{Directive, FileExemption};
use crate::noqa::{Directive, FileExemption, NoqaDirectives, NoqaMapping};
use crate::registry::{AsRule, Rule};
use crate::rule_redirects::get_redirect_target;
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA};
@@ -17,37 +15,25 @@ use crate::settings::{flags, Settings};
pub fn check_noqa(
diagnostics: &mut Vec<Diagnostic>,
contents: &str,
commented_lines: &[usize],
noqa_line_for: &IntMap<usize, usize>,
locator: &Locator,
comment_ranges: &[TextRange],
noqa_line_for: &NoqaMapping,
settings: &Settings,
autofix: flags::Autofix,
) -> Vec<usize> {
let enforce_noqa = settings.rules.enabled(Rule::UnusedNOQA);
let lines: Vec<&str> = contents.universal_newlines().collect();
// Identify any codes that are globally exempted (within the current file).
let exemption = noqa::file_exemption(&lines, commented_lines);
// Map from line number to `noqa` directive on that line, along with any codes
// that were matched by the directive.
let mut noqa_directives: IntMap<usize, (Directive, Vec<NoqaCode>)> = IntMap::default();
let exemption = noqa::file_exemption(locator.contents(), comment_ranges);
// Extract all `noqa` directives.
if enforce_noqa {
for lineno in commented_lines {
noqa_directives
.entry(lineno - 1)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[lineno - 1]), vec![]));
}
}
let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, locator);
// Indices of diagnostics that were ignored by a `noqa` directive.
let mut ignored_diagnostics = vec![];
// Remove any ignored diagnostics.
for (index, diagnostic) in diagnostics.iter().enumerate() {
'outer: for (index, diagnostic) in diagnostics.iter().enumerate() {
if matches!(diagnostic.kind.rule(), Rule::BlanketNOQA) {
continue;
}
@@ -68,92 +54,65 @@ pub fn check_noqa(
FileExemption::None => {}
}
let diagnostic_lineno = diagnostic.location.row();
let noqa_offsets = diagnostic
.parent
.into_iter()
.chain(std::iter::once(diagnostic.start()))
.map(|position| noqa_line_for.resolve(position))
.unique();
// Is the violation ignored by a `noqa` directive on the parent line?
if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) {
if parent_lineno != diagnostic_lineno {
let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno);
if commented_lines.contains(noqa_lineno) {
let noqa = noqa_directives.entry(noqa_lineno - 1).or_insert_with(|| {
(noqa::extract_noqa_directive(lines[noqa_lineno - 1]), vec![])
});
match noqa {
(Directive::All(..), matches) => {
matches.push(diagnostic.kind.rule().noqa_code());
ignored_diagnostics.push(index);
continue;
}
(Directive::Codes(.., codes, _), matches) => {
if noqa::includes(diagnostic.kind.rule(), codes) {
matches.push(diagnostic.kind.rule().noqa_code());
ignored_diagnostics.push(index);
continue;
}
}
(Directive::None, ..) => {}
}
}
}
}
// Is the diagnostic ignored by a `noqa` directive on the same line?
let noqa_lineno = noqa_line_for
.get(&diagnostic_lineno)
.unwrap_or(&diagnostic_lineno);
if commented_lines.contains(noqa_lineno) {
let noqa = noqa_directives
.entry(noqa_lineno - 1)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno - 1]), vec![]));
match noqa {
(Directive::All(..), matches) => {
matches.push(diagnostic.kind.rule().noqa_code());
ignored_diagnostics.push(index);
continue;
}
(Directive::Codes(.., codes, _), matches) => {
if noqa::includes(diagnostic.kind.rule(), codes) {
matches.push(diagnostic.kind.rule().noqa_code());
for noqa_offset in noqa_offsets {
if let Some(directive_line) = noqa_directives.find_line_with_directive_mut(noqa_offset)
{
let suppressed = match &directive_line.directive {
Directive::All(..) => {
directive_line
.matches
.push(diagnostic.kind.rule().noqa_code());
ignored_diagnostics.push(index);
continue;
true
}
Directive::Codes(.., codes, _) => {
if noqa::includes(diagnostic.kind.rule(), codes) {
directive_line
.matches
.push(diagnostic.kind.rule().noqa_code());
ignored_diagnostics.push(index);
true
} else {
false
}
}
Directive::None => unreachable!(),
};
if suppressed {
continue 'outer;
}
(Directive::None, ..) => {}
}
}
}
// Enforce that the noqa directive was actually used (RUF100).
if enforce_noqa {
for (row, (directive, matches)) in noqa_directives {
match directive {
Directive::All(leading_spaces, start_byte, end_byte, trailing_spaces) => {
if matches.is_empty() {
let start_char = lines[row][..start_byte].chars().count();
let end_char =
start_char + lines[row][start_byte..end_byte].chars().count();
let mut diagnostic = Diagnostic::new(
UnusedNOQA { codes: None },
Range::new(
Location::new(row + 1, start_char),
Location::new(row + 1, end_char),
),
);
for line in noqa_directives.lines() {
match &line.directive {
Directive::All(leading_spaces, noqa_range, trailing_spaces) => {
if line.matches.is_empty() {
let mut diagnostic =
Diagnostic::new(UnusedNOQA { codes: None }, *noqa_range);
if autofix.into() && settings.rules.should_fix(diagnostic.kind.rule()) {
diagnostic.set_fix(delete_noqa(
row,
lines[row],
leading_spaces,
start_byte,
end_byte,
trailing_spaces,
*leading_spaces,
*noqa_range,
*trailing_spaces,
locator,
));
}
diagnostics.push(diagnostic);
}
}
Directive::Codes(leading_spaces, start_byte, end_byte, codes, trailing_spaces) => {
Directive::Codes(leading_spaces, range, codes, trailing_spaces) => {
let mut disabled_codes = vec![];
let mut unknown_codes = vec![];
let mut unmatched_codes = vec![];
@@ -166,7 +125,9 @@ pub fn check_noqa(
break;
}
if matches.iter().any(|m| *m == code) || settings.external.contains(code) {
if line.matches.iter().any(|m| *m == code)
|| settings.external.contains(code)
{
valid_codes.push(code);
} else {
if let Ok(rule) = Rule::from_code(code) {
@@ -189,10 +150,6 @@ pub fn check_noqa(
&& unknown_codes.is_empty()
&& unmatched_codes.is_empty())
{
let start_char = lines[row][..start_byte].chars().count();
let end_char =
start_char + lines[row][start_byte..end_byte].chars().count();
let mut diagnostic = Diagnostic::new(
UnusedNOQA {
codes: Some(UnusedCodes {
@@ -210,26 +167,20 @@ pub fn check_noqa(
.collect(),
}),
},
Range::new(
Location::new(row + 1, start_char),
Location::new(row + 1, end_char),
),
*range,
);
if autofix.into() && settings.rules.should_fix(diagnostic.kind.rule()) {
if valid_codes.is_empty() {
diagnostic.set_fix(delete_noqa(
row,
lines[row],
leading_spaces,
start_byte,
end_byte,
trailing_spaces,
*leading_spaces,
*range,
*trailing_spaces,
locator,
));
} else {
diagnostic.set_fix(Edit::replacement(
diagnostic.set_fix(Edit::range_replacement(
format!("# noqa: {}", valid_codes.join(", ")),
Location::new(row + 1, start_char),
Location::new(row + 1, end_char),
*range,
));
}
}
@@ -247,39 +198,37 @@ pub fn check_noqa(
/// Generate a [`Edit`] to delete a `noqa` directive.
fn delete_noqa(
row: usize,
line: &str,
leading_spaces: usize,
start_byte: usize,
end_byte: usize,
trailing_spaces: usize,
leading_spaces: TextSize,
noqa_range: TextRange,
trailing_spaces: TextSize,
locator: &Locator,
) -> Edit {
if start_byte - leading_spaces == 0 && end_byte == line.len() {
// Ex) `# noqa`
Edit::deletion(Location::new(row + 1, 0), Location::new(row + 2, 0))
} else if end_byte == line.len() {
// Ex) `x = 1 # noqa`
let start_char = line[..start_byte].chars().count();
let end_char = start_char + line[start_byte..end_byte].chars().count();
Edit::deletion(
Location::new(row + 1, start_char - leading_spaces),
Location::new(row + 1, end_char + trailing_spaces),
let line_range = locator.line_range(noqa_range.start());
// Ex) `# noqa`
if line_range
== TextRange::new(
noqa_range.start() - leading_spaces,
noqa_range.end() + trailing_spaces,
)
} else if line[end_byte..].trim_start().starts_with('#') {
// Ex) `x = 1 # noqa # type: ignore`
let start_char = line[..start_byte].chars().count();
let end_char = start_char + line[start_byte..end_byte].chars().count();
{
let full_line_end = locator.full_line_end(line_range.end());
Edit::deletion(line_range.start(), full_line_end)
}
// Ex) `x = 1 # noqa`
else if noqa_range.end() + trailing_spaces == line_range.end() {
Edit::deletion(noqa_range.start() - leading_spaces, line_range.end())
}
// Ex) `x = 1 # noqa # type: ignore`
else if locator.contents()[usize::from(noqa_range.end() + trailing_spaces)..].starts_with('#')
{
Edit::deletion(noqa_range.start(), noqa_range.end() + trailing_spaces)
}
// Ex) `x = 1 # noqa here`
else {
Edit::deletion(
Location::new(row + 1, start_char),
Location::new(row + 1, end_char + trailing_spaces),
)
} else {
// Ex) `x = 1 # noqa here`
let start_char = line[..start_byte].chars().count();
let end_char = start_char + line[start_byte..end_byte].chars().count();
Edit::deletion(
Location::new(row + 1, start_char + 1 + 1),
Location::new(row + 1, end_char + trailing_spaces),
noqa_range.start() + "# ".text_len(),
noqa_range.end() + trailing_spaces,
)
}
}

View File

@@ -1,5 +1,6 @@
//! Lint rules based on checking physical lines.
use ruff_text_size::TextSize;
use std::path::Path;
use ruff_diagnostics::Diagnostic;
@@ -25,7 +26,7 @@ pub fn check_physical_lines(
locator: &Locator,
stylist: &Stylist,
indexer: &Indexer,
doc_lines: &[usize],
doc_lines: &[TextSize],
settings: &Settings,
autofix: flags::Autofix,
) -> Vec<Diagnostic> {
@@ -55,20 +56,19 @@ pub fn check_physical_lines(
let fix_shebang_whitespace =
autofix.into() && settings.rules.should_fix(Rule::ShebangLeadingWhitespace);
let mut commented_lines_iter = indexer.commented_lines().iter().peekable();
let mut commented_lines_iter = indexer.comment_ranges().iter().peekable();
let mut doc_lines_iter = doc_lines.iter().peekable();
let string_lines = indexer.string_ranges();
let string_lines = indexer.triple_quoted_string_ranges();
for (index, line) in locator.contents().universal_newlines().enumerate() {
while commented_lines_iter
.next_if(|lineno| &(index + 1) == *lineno)
.next_if(|comment_range| line.range().contains_range(**comment_range))
.is_some()
{
if enforce_unnecessary_coding_comment {
if index < 2 {
if let Some(diagnostic) =
unnecessary_coding_comment(index, line, fix_unnecessary_coding_comment)
unnecessary_coding_comment(&line, fix_unnecessary_coding_comment)
{
diagnostics.push(diagnostic);
}
@@ -76,11 +76,11 @@ pub fn check_physical_lines(
}
if enforce_blanket_type_ignore {
blanket_type_ignore(&mut diagnostics, index, line);
blanket_type_ignore(&mut diagnostics, &line);
}
if enforce_blanket_noqa {
blanket_noqa(&mut diagnostics, index, line);
blanket_noqa(&mut diagnostics, &line);
}
if enforce_shebang_missing
@@ -89,31 +89,31 @@ pub fn check_physical_lines(
|| enforce_shebang_newline
|| enforce_shebang_python
{
let shebang = extract_shebang(line);
let shebang = extract_shebang(&line);
if enforce_shebang_not_executable {
if let Some(diagnostic) = shebang_not_executable(path, index, &shebang) {
if let Some(diagnostic) = shebang_not_executable(path, line.range(), &shebang) {
diagnostics.push(diagnostic);
}
}
if enforce_shebang_missing {
if !has_any_shebang && matches!(shebang, ShebangDirective::Match(_, _, _, _)) {
if !has_any_shebang && matches!(shebang, ShebangDirective::Match(..)) {
has_any_shebang = true;
}
}
if enforce_shebang_whitespace {
if let Some(diagnostic) =
shebang_whitespace(index, &shebang, fix_shebang_whitespace)
shebang_whitespace(line.range(), &shebang, fix_shebang_whitespace)
{
diagnostics.push(diagnostic);
}
}
if enforce_shebang_newline {
if let Some(diagnostic) = shebang_newline(index, &shebang) {
if let Some(diagnostic) = shebang_newline(line.range(), &shebang, index == 0) {
diagnostics.push(diagnostic);
}
}
if enforce_shebang_python {
if let Some(diagnostic) = shebang_python(index, &shebang) {
if let Some(diagnostic) = shebang_python(line.range(), &shebang) {
diagnostics.push(diagnostic);
}
}
@@ -121,40 +121,40 @@ pub fn check_physical_lines(
}
while doc_lines_iter
.next_if(|lineno| &(index + 1) == *lineno)
.next_if(|doc_line_start| line.range().contains(**doc_line_start))
.is_some()
{
if enforce_doc_line_too_long {
if let Some(diagnostic) = doc_line_too_long(index, line, settings) {
if let Some(diagnostic) = doc_line_too_long(&line, settings) {
diagnostics.push(diagnostic);
}
}
}
if enforce_mixed_spaces_and_tabs {
if let Some(diagnostic) = mixed_spaces_and_tabs(index, line) {
if let Some(diagnostic) = mixed_spaces_and_tabs(&line) {
diagnostics.push(diagnostic);
}
}
if enforce_line_too_long {
if let Some(diagnostic) = line_too_long(index, line, settings) {
if let Some(diagnostic) = line_too_long(&line, settings) {
diagnostics.push(diagnostic);
}
}
if enforce_bidirectional_unicode {
diagnostics.extend(pylint::rules::bidirectional_unicode(index, line));
diagnostics.extend(pylint::rules::bidirectional_unicode(&line));
}
if enforce_trailing_whitespace || enforce_blank_line_contains_whitespace {
if let Some(diagnostic) = trailing_whitespace(index, line, settings, autofix) {
if let Some(diagnostic) = trailing_whitespace(&line, settings, autofix) {
diagnostics.push(diagnostic);
}
}
if enforce_tab_indentation {
if let Some(diagnostic) = tab_indentation(index + 1, line, string_lines) {
if let Some(diagnostic) = tab_indentation(&line, string_lines) {
diagnostics.push(diagnostic);
}
}
@@ -197,7 +197,7 @@ mod tests {
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
let locator = Locator::new(line);
let tokens: Vec<_> = lex(line, Mode::Module).collect();
let indexer: Indexer = tokens.as_slice().into();
let indexer = Indexer::from_tokens(&tokens, &locator);
let stylist = Stylist::from_tokens(&tokens, &locator);
let check_with_max_line_length = |line_length: usize| {

View File

@@ -64,7 +64,7 @@ pub fn check_tokens(
// RUF001, RUF002, RUF003
if enforce_ambiguous_unicode_character {
let mut state_machine = StateMachine::default();
for &(start, ref tok, end) in tokens.iter().flatten() {
for &(ref tok, range) in tokens.iter().flatten() {
let is_docstring = if enforce_ambiguous_unicode_character {
state_machine.consume(tok)
} else {
@@ -74,8 +74,7 @@ pub fn check_tokens(
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
diagnostics.extend(ruff::rules::ambiguous_unicode_character(
locator,
start,
end,
range,
if matches!(tok, Tok::String { .. }) {
if is_docstring {
Context::Docstring
@@ -94,10 +93,10 @@ pub fn check_tokens(
// ERA001
if enforce_commented_out_code {
for (start, tok, end) in tokens.iter().flatten() {
for (tok, range) in tokens.iter().flatten() {
if matches!(tok, Tok::Comment(_)) {
if let Some(diagnostic) =
eradicate::rules::commented_out_code(locator, *start, *end, settings, autofix)
eradicate::rules::commented_out_code(locator, *range, settings, autofix)
{
diagnostics.push(diagnostic);
}
@@ -107,12 +106,11 @@ pub fn check_tokens(
// W605
if enforce_invalid_escape_sequence {
for (start, tok, end) in tokens.iter().flatten() {
for (tok, range) in tokens.iter().flatten() {
if matches!(tok, Tok::String { .. }) {
diagnostics.extend(pycodestyle::rules::invalid_escape_sequence(
locator,
*start,
*end,
*range,
autofix.into() && settings.rules.should_fix(Rule::InvalidEscapeSequence),
));
}
@@ -120,10 +118,10 @@ pub fn check_tokens(
}
// PLE2510, PLE2512, PLE2513
if enforce_invalid_string_character {
for (start, tok, end) in tokens.iter().flatten() {
for (tok, range) in tokens.iter().flatten() {
if matches!(tok, Tok::String { .. }) {
diagnostics.extend(
pylint::rules::invalid_string_characters(locator, *start, *end, autofix.into())
pylint::rules::invalid_string_characters(locator, *range, autofix.into())
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);
@@ -155,6 +153,7 @@ pub fn check_tokens(
flake8_implicit_str_concat::rules::implicit(
tokens,
&settings.flake8_implicit_str_concat,
locator,
)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),

View File

@@ -1,8 +1,15 @@
use crate::registry::{Linter, Rule};
use std::fmt::Formatter;
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub struct NoqaCode(&'static str, &'static str);
impl std::fmt::Debug for NoqaCode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl std::fmt::Display for NoqaCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{}{}", self.0, self.1)
@@ -197,11 +204,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Pylint, "R5501") => Rule::CollapsibleElseIf,
(Pylint, "W0120") => Rule::UselessElseOnLoop,
(Pylint, "W0129") => Rule::AssertOnStringLiteral,
(Pylint, "W0406") => Rule::ImportSelf,
(Pylint, "W0602") => Rule::GlobalVariableNotAssigned,
(Pylint, "W0603") => Rule::GlobalStatement,
(Pylint, "W0711") => Rule::BinaryOpException,
(Pylint, "W1508") => Rule::InvalidEnvvarDefault,
(Pylint, "W2901") => Rule::RedefinedLoopName,
(Pylint, "E0302") => Rule::UnexpectedSpecialMethodSignature,
// flake8-builtins
(Flake8Builtins, "001") => Rule::BuiltinVariableShadowing,
@@ -534,6 +543,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
// flake8-import-conventions
(Flake8ImportConventions, "001") => Rule::UnconventionalImportAlias,
(Flake8ImportConventions, "002") => Rule::BannedImportAlias,
(Flake8ImportConventions, "003") => Rule::BannedImportFrom,
// flake8-datetimez
(Flake8Datetimez, "001") => Rule::CallDatetimeWithoutTzinfo,
@@ -583,8 +593,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Flake8Pyi, "014") => Rule::ArgumentDefaultInStub,
(Flake8Pyi, "015") => Rule::AssignmentDefaultInStub,
(Flake8Pyi, "016") => Rule::DuplicateUnionMember,
(Flake8Pyi, "020") => Rule::QuotedAnnotationInStub,
(Flake8Pyi, "021") => Rule::DocstringInStub,
(Flake8Pyi, "033") => Rule::TypeCommentInStub,
(Flake8Pyi, "042") => Rule::SnakeCaseTypeAlias,
(Flake8Pyi, "043") => Rule::TSuffixedTypeAlias,
// flake8-pytest-style
(Flake8PytestStyle, "001") => Rule::PytestFixtureIncorrectParenthesesStyle,

View File

@@ -1,16 +1,18 @@
//! Extract `# noqa` and `# isort: skip` directives from tokenized source.
use crate::noqa::NoqaMapping;
use bitflags::bitflags;
use nohash_hasher::{IntMap, IntSet};
use rustpython_parser::ast::Location;
use ruff_python_ast::source_code::{Indexer, Locator};
use ruff_text_size::{TextLen, TextRange, TextSize};
use rustpython_parser::lexer::LexResult;
use rustpython_parser::Tok;
use crate::settings::Settings;
bitflags! {
pub struct Flags: u32 {
const NOQA = 0b0000_0001;
#[derive(Debug, Copy, Clone)]
pub struct Flags: u8 {
const NOQA = 0b0000_0001;
const ISORT = 0b0000_0010;
}
}
@@ -29,27 +31,50 @@ impl Flags {
}
}
#[derive(Default)]
#[derive(Default, Debug)]
pub struct IsortDirectives {
pub exclusions: IntSet<usize>,
pub splits: Vec<usize>,
/// Ranges for which sorting is disabled
pub exclusions: Vec<TextRange>,
/// Text positions at which splits should be inserted
pub splits: Vec<TextSize>,
pub skip_file: bool,
}
impl IsortDirectives {
pub fn is_excluded(&self, offset: TextSize) -> bool {
for range in &self.exclusions {
if range.contains(offset) {
return true;
}
if range.start() > offset {
break;
}
}
false
}
}
pub struct Directives {
pub noqa_line_for: IntMap<usize, usize>,
pub noqa_line_for: NoqaMapping,
pub isort: IsortDirectives,
}
pub fn extract_directives(lxr: &[LexResult], flags: Flags) -> Directives {
pub fn extract_directives(
lxr: &[LexResult],
flags: Flags,
locator: &Locator,
indexer: &Indexer,
) -> Directives {
Directives {
noqa_line_for: if flags.contains(Flags::NOQA) {
extract_noqa_line_for(lxr)
extract_noqa_line_for(lxr, locator, indexer)
} else {
IntMap::default()
NoqaMapping::default()
},
isort: if flags.contains(Flags::ISORT) {
extract_isort_directives(lxr)
extract_isort_directives(lxr, locator)
} else {
IsortDirectives::default()
},
@@ -57,48 +82,92 @@ pub fn extract_directives(lxr: &[LexResult], flags: Flags) -> Directives {
}
/// Extract a mapping from logical line to noqa line.
pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap<usize, usize> {
let mut noqa_line_for: IntMap<usize, usize> = IntMap::default();
let mut prev_non_newline: Option<(&Location, &Tok, &Location)> = None;
for (start, tok, end) in lxr.iter().flatten() {
if matches!(tok, Tok::EndOfFile) {
break;
}
// For multi-line strings, we expect `noqa` directives on the last line of the
// string.
if matches!(tok, Tok::String { .. }) && end.row() > start.row() {
for i in start.row()..end.row() {
noqa_line_for.insert(i, end.row());
pub fn extract_noqa_line_for(
lxr: &[LexResult],
locator: &Locator,
indexer: &Indexer,
) -> NoqaMapping {
let mut string_mappings = Vec::new();
for (tok, range) in lxr.iter().flatten() {
match tok {
Tok::EndOfFile => {
break;
}
}
// For continuations, we expect `noqa` directives on the last line of the
// continuation.
if matches!(
tok,
Tok::Newline | Tok::NonLogicalNewline | Tok::Comment(..)
) {
if let Some((.., end)) = prev_non_newline {
for i in end.row()..start.row() {
noqa_line_for.insert(i, start.row());
// For multi-line strings, we expect `noqa` directives on the last line of the
// string.
Tok::String {
triple_quoted: true,
..
} => {
if locator.contains_line_break(*range) {
string_mappings.push(*range);
}
}
prev_non_newline = None;
} else if prev_non_newline.is_none() {
prev_non_newline = Some((start, tok, end));
_ => {}
}
}
noqa_line_for
let mut continuation_mappings = Vec::new();
// For continuations, we expect `noqa` directives on the last line of the
// continuation.
let mut last: Option<TextRange> = None;
for continuation_line in indexer.continuation_line_starts() {
let line_end = locator.full_line_end(*continuation_line);
if let Some(last_range) = last.take() {
if last_range.end() == *continuation_line {
last = Some(TextRange::new(last_range.start(), line_end));
continue;
}
// new continuation
continuation_mappings.push(last_range);
}
last = Some(TextRange::new(*continuation_line, line_end));
}
if let Some(last_range) = last.take() {
continuation_mappings.push(last_range);
}
// Merge the mappings in sorted order
let mut mappings =
NoqaMapping::with_capacity(continuation_mappings.len() + string_mappings.len());
let mut continuation_mappings = continuation_mappings.into_iter().peekable();
let mut string_mappings = string_mappings.into_iter().peekable();
while let (Some(continuation), Some(string)) =
(continuation_mappings.peek(), string_mappings.peek())
{
if continuation.start() <= string.start() {
mappings.push_mapping(continuation_mappings.next().unwrap());
} else {
mappings.push_mapping(string_mappings.next().unwrap());
}
}
for mapping in continuation_mappings {
mappings.push_mapping(mapping);
}
for mapping in string_mappings {
mappings.push_mapping(mapping);
}
mappings
}
/// Extract a set of lines over which to disable isort.
pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
let mut exclusions: IntSet<usize> = IntSet::default();
let mut splits: Vec<usize> = Vec::default();
let mut off: Option<Location> = None;
let mut last: Option<Location> = None;
for &(start, ref tok, end) in lxr.iter().flatten() {
last = Some(end);
/// Extract a set of ranges over which to disable isort.
pub fn extract_isort_directives(lxr: &[LexResult], locator: &Locator) -> IsortDirectives {
let mut exclusions: Vec<TextRange> = Vec::default();
let mut splits: Vec<TextSize> = Vec::default();
let mut off: Option<TextSize> = None;
for &(ref tok, range) in lxr.iter().flatten() {
let Tok::Comment(comment_text) = tok else {
continue;
};
@@ -108,7 +177,7 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
// required to include the space, and must appear on their own lines.
let comment_text = comment_text.trim_end();
if matches!(comment_text, "# isort: split" | "# ruff: isort: split") {
splits.push(start.row());
splits.push(range.start());
} else if matches!(
comment_text,
"# isort: skip_file"
@@ -122,30 +191,25 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
};
} else if off.is_some() {
if comment_text == "# isort: on" || comment_text == "# ruff: isort: on" {
if let Some(start) = off {
for row in start.row() + 1..=end.row() {
exclusions.insert(row);
}
if let Some(exclusion_start) = off {
exclusions.push(TextRange::new(exclusion_start, range.start()));
}
off = None;
}
} else {
if comment_text.contains("isort: skip") || comment_text.contains("isort:skip") {
exclusions.insert(start.row());
exclusions.push(locator.line_range(range.start()));
} else if comment_text == "# isort: off" || comment_text == "# ruff: isort: off" {
off = Some(start);
off = Some(range.start());
}
}
}
if let Some(start) = off {
// Enforce unterminated `isort: off`.
if let Some(end) = last {
for row in start.row() + 1..=end.row() {
exclusions.insert(row);
}
}
exclusions.push(TextRange::new(start, locator.contents().text_len()));
}
IsortDirectives {
exclusions,
splits,
@@ -155,120 +219,98 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
#[cfg(test)]
mod tests {
use nohash_hasher::{IntMap, IntSet};
use ruff_python_ast::source_code::{Indexer, Locator};
use ruff_text_size::{TextLen, TextRange, TextSize};
use rustpython_parser::lexer::LexResult;
use rustpython_parser::{lexer, Mode};
use crate::directives::{extract_isort_directives, extract_noqa_line_for};
use crate::noqa::NoqaMapping;
fn noqa_mappings(contents: &str) -> NoqaMapping {
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
let locator = Locator::new(contents);
let indexer = Indexer::from_tokens(&lxr, &locator);
extract_noqa_line_for(&lxr, &locator, &indexer)
}
#[test]
fn noqa_extraction() {
let lxr: Vec<LexResult> = lexer::lex(
"x = 1
y = 2
z = x + 1",
Mode::Module,
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), IntMap::default());
let contents = "x = 1
y = 2 \
+ 1
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(
"
assert_eq!(noqa_mappings(contents), NoqaMapping::default());
let contents = "
x = 1
y = 2
z = x + 1",
Mode::Module,
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), IntMap::default());
z = x + 1";
assert_eq!(noqa_mappings(contents), NoqaMapping::default());
let lxr: Vec<LexResult> = lexer::lex(
"x = 1
let contents = "x = 1
y = 2
z = x + 1
",
Mode::Module,
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), IntMap::default());
";
assert_eq!(noqa_mappings(contents), NoqaMapping::default());
let lxr: Vec<LexResult> = lexer::lex(
"x = 1
let contents = "x = 1
y = 2
z = x + 1
",
Mode::Module,
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), IntMap::default());
";
assert_eq!(noqa_mappings(contents), NoqaMapping::default());
let lxr: Vec<LexResult> = lexer::lex(
"x = '''abc
let contents = "x = '''abc
def
ghi
'''
y = 2
z = x + 1",
Mode::Module,
)
.collect();
z = x + 1";
assert_eq!(
extract_noqa_line_for(&lxr),
IntMap::from_iter([(1, 4), (2, 4), (3, 4)])
noqa_mappings(contents),
NoqaMapping::from_iter([TextRange::new(TextSize::from(4), TextSize::from(22)),])
);
let lxr: Vec<LexResult> = lexer::lex(
"x = 1
let contents = "x = 1
y = '''abc
def
ghi
'''
z = 2",
Mode::Module,
)
.collect();
z = 2";
assert_eq!(
extract_noqa_line_for(&lxr),
IntMap::from_iter([(2, 5), (3, 5), (4, 5)])
noqa_mappings(contents),
NoqaMapping::from_iter([TextRange::new(TextSize::from(10), TextSize::from(28))])
);
let lxr: Vec<LexResult> = lexer::lex(
"x = 1
let contents = "x = 1
y = '''abc
def
ghi
'''",
Mode::Module,
)
.collect();
'''";
assert_eq!(
extract_noqa_line_for(&lxr),
IntMap::from_iter([(2, 5), (3, 5), (4, 5)])
noqa_mappings(contents),
NoqaMapping::from_iter([TextRange::new(TextSize::from(10), TextSize::from(28))])
);
let lxr: Vec<LexResult> = lexer::lex(
r#"x = \
1"#,
Mode::Module,
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), IntMap::from_iter([(1, 2)]));
let contents = r#"x = \
1"#;
assert_eq!(
noqa_mappings(contents),
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(6))])
);
let lxr: Vec<LexResult> = lexer::lex(
r#"from foo import \
let contents = r#"from foo import \
bar as baz, \
qux as quux"#,
Mode::Module,
)
.collect();
qux as quux"#;
assert_eq!(
extract_noqa_line_for(&lxr),
IntMap::from_iter([(1, 3), (2, 3)])
noqa_mappings(contents),
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(36))])
);
let lxr: Vec<LexResult> = lexer::lex(
r#"
let contents = r#"
# Foo
from foo import \
bar as baz, \
@@ -276,13 +318,14 @@ from foo import \
x = \
1
y = \
2"#,
Mode::Module,
)
.collect();
2"#;
assert_eq!(
extract_noqa_line_for(&lxr),
IntMap::from_iter([(3, 5), (4, 5), (6, 7), (8, 9)])
noqa_mappings(contents),
NoqaMapping::from_iter([
TextRange::new(TextSize::from(7), TextSize::from(43)),
TextRange::new(TextSize::from(65), TextSize::from(71)),
TextRange::new(TextSize::from(77), TextSize::from(83)),
])
);
}
@@ -292,7 +335,10 @@ y = \
y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default());
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
Vec::default()
);
let contents = "# isort: off
x = 1
@@ -301,8 +347,8 @@ y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
assert_eq!(
extract_isort_directives(&lxr).exclusions,
IntSet::from_iter([2, 3, 4])
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
Vec::from_iter([TextRange::new(TextSize::from(0), TextSize::from(25))])
);
let contents = "# isort: off
@@ -314,8 +360,8 @@ z = x + 1
# isort: on";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
assert_eq!(
extract_isort_directives(&lxr).exclusions,
IntSet::from_iter([2, 3, 4, 5])
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
Vec::from_iter([TextRange::new(TextSize::from(0), TextSize::from(38))])
);
let contents = "# isort: off
@@ -324,8 +370,8 @@ y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
assert_eq!(
extract_isort_directives(&lxr).exclusions,
IntSet::from_iter([2, 3, 4])
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
Vec::from_iter([TextRange::at(TextSize::from(0), contents.text_len())])
);
let contents = "# isort: skip_file
@@ -333,7 +379,10 @@ x = 1
y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default());
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
Vec::default()
);
let contents = "# isort: off
x = 1
@@ -342,7 +391,10 @@ y = 2
# isort: skip_file
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default());
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
Vec::default()
);
}
#[test]
@@ -351,19 +403,28 @@ z = x + 1";
y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
assert_eq!(extract_isort_directives(&lxr).splits, Vec::<usize>::new());
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).splits,
Vec::new()
);
let contents = "x = 1
y = 2
# isort: split
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
assert_eq!(extract_isort_directives(&lxr).splits, vec![3]);
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).splits,
vec![TextSize::from(12)]
);
let contents = "x = 1
y = 2 # isort: split
z = x + 1";
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
assert_eq!(extract_isort_directives(&lxr).splits, vec![2]);
assert_eq!(
extract_isort_directives(&lxr, &Locator::new(contents)).splits,
vec![TextSize::from(13)]
);
}
}

View File

@@ -1,8 +1,10 @@
//! Doc line extraction. In this context, a doc line is a line consisting of a
//! standalone comment or a constant string statement.
use ruff_text_size::{TextRange, TextSize};
use std::iter::FusedIterator;
use ruff_python_ast::source_code::Locator;
use rustpython_parser::ast::{Constant, ExprKind, Stmt, StmtKind, Suite};
use rustpython_parser::lexer::LexResult;
use rustpython_parser::Tok;
@@ -11,46 +13,56 @@ use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
/// Extract doc lines (standalone comments) from a token sequence.
pub fn doc_lines_from_tokens(lxr: &[LexResult]) -> DocLines {
DocLines::new(lxr)
pub fn doc_lines_from_tokens<'a>(lxr: &'a [LexResult], locator: &'a Locator<'a>) -> DocLines<'a> {
DocLines::new(lxr, locator)
}
pub struct DocLines<'a> {
inner: std::iter::Flatten<core::slice::Iter<'a, LexResult>>,
prev: Option<usize>,
locator: &'a Locator<'a>,
prev: TextSize,
}
impl<'a> DocLines<'a> {
fn new(lxr: &'a [LexResult]) -> Self {
fn new(lxr: &'a [LexResult], locator: &'a Locator) -> Self {
Self {
inner: lxr.iter().flatten(),
prev: None,
locator,
prev: TextSize::default(),
}
}
}
impl Iterator for DocLines<'_> {
type Item = usize;
type Item = TextSize;
fn next(&mut self) -> Option<Self::Item> {
let mut at_start_of_line = true;
loop {
let (start, tok, end) = self.inner.next()?;
let (tok, range) = self.inner.next()?;
match tok {
Tok::Indent | Tok::Dedent | Tok::Newline => continue,
Tok::Comment(..) => {
if let Some(prev) = self.prev {
if start.row() > prev {
break Some(start.row());
}
} else {
break Some(start.row());
if at_start_of_line
|| self
.locator
.contains_line_break(TextRange::new(self.prev, range.start()))
{
break Some(range.start());
}
}
_ => {}
Tok::Newline => {
at_start_of_line = true;
}
Tok::Indent | Tok::Dedent => {
// ignore
}
_ => {
at_start_of_line = false;
}
}
self.prev = Some(end.row());
self.prev = range.end();
}
}
}
@@ -59,7 +71,7 @@ impl FusedIterator for DocLines<'_> {}
#[derive(Default)]
struct StringLinesVisitor {
string_lines: Vec<usize>,
string_lines: Vec<TextSize>,
}
impl Visitor<'_> for StringLinesVisitor {
@@ -70,16 +82,15 @@ impl Visitor<'_> for StringLinesVisitor {
..
} = &value.node
{
self.string_lines
.extend(value.location.row()..=value.end_location.unwrap().row());
self.string_lines.push(value.start());
}
}
visitor::walk_stmt(self, stmt);
}
}
/// Extract doc lines (standalone strings) from an AST.
pub fn doc_lines_from_ast(python_ast: &Suite) -> Vec<usize> {
/// Extract doc lines (standalone strings) start positions from an AST.
pub fn doc_lines_from_ast(python_ast: &Suite) -> Vec<TextSize> {
let mut visitor = StringLinesVisitor::default();
visitor.visit_body(python_ast);
visitor.string_lines

View File

@@ -1,4 +1,7 @@
use ruff_text_size::{TextRange, TextSize};
use rustpython_parser::ast::{Expr, Stmt};
use std::fmt::{Debug, Formatter};
use std::ops::Deref;
use ruff_python_semantic::analyze::visibility::{
class_visibility, function_visibility, method_visibility, Modifier, Visibility, VisibleScope,
@@ -25,11 +28,78 @@ pub struct Definition<'a> {
pub struct Docstring<'a> {
pub kind: DefinitionKind<'a>,
pub expr: &'a Expr,
/// The content of the docstring, including the leading and trailing quotes.
pub contents: &'a str,
pub body: &'a str,
/// The range of the docstring body (without the quotes). The range is relative to [`Self::contents`].
pub body_range: TextRange,
pub indentation: &'a str,
}
impl<'a> Docstring<'a> {
pub fn body(&self) -> DocstringBody {
DocstringBody { docstring: self }
}
pub const fn start(&self) -> TextSize {
self.expr.start()
}
pub const fn end(&self) -> TextSize {
self.expr.end()
}
pub const fn range(&self) -> TextRange {
self.expr.range()
}
pub fn leading_quote(&self) -> &'a str {
&self.contents[TextRange::up_to(self.body_range.start())]
}
}
#[derive(Copy, Clone)]
pub struct DocstringBody<'a> {
docstring: &'a Docstring<'a>,
}
impl<'a> DocstringBody<'a> {
#[inline]
pub fn start(self) -> TextSize {
self.range().start()
}
#[inline]
pub fn end(self) -> TextSize {
self.range().end()
}
pub fn range(self) -> TextRange {
self.docstring.body_range + self.docstring.start()
}
pub fn as_str(self) -> &'a str {
&self.docstring.contents[self.docstring.body_range]
}
}
impl Deref for DocstringBody<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl Debug for DocstringBody<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DocstringBody")
.field("text", &self.as_str())
.field("range", &self.range())
.finish()
}
}
#[derive(Copy, Clone)]
pub enum Documentable {
Class,

View File

@@ -26,6 +26,8 @@ pub(crate) static GOOGLE_SECTIONS: &[SectionKind] = &[
SectionKind::KeywordArguments,
SectionKind::Note,
SectionKind::Notes,
SectionKind::OtherArgs,
SectionKind::OtherArguments,
SectionKind::Return,
SectionKind::Tip,
SectionKind::Todo,

View File

@@ -14,6 +14,7 @@ pub(crate) static NUMPY_SECTIONS: &[SectionKind] = &[
SectionKind::Yields,
// NumPy-only
SectionKind::ExtendedSummary,
SectionKind::OtherParams,
SectionKind::OtherParameters,
SectionKind::Parameters,
SectionKind::ShortSummary,

View File

@@ -1,5 +1,10 @@
use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator};
use ruff_text_size::{TextLen, TextRange, TextSize};
use std::fmt::{Debug, Formatter};
use std::iter::FusedIterator;
use strum_macros::EnumIter;
use crate::docstrings::definition::{Docstring, DocstringBody};
use crate::docstrings::styles::SectionStyle;
use ruff_python_ast::whitespace;
@@ -22,6 +27,9 @@ pub enum SectionKind {
Methods,
Note,
Notes,
OtherArgs,
OtherArguments,
OtherParams,
OtherParameters,
Parameters,
Raises,
@@ -59,6 +67,9 @@ impl SectionKind {
"methods" => Some(Self::Methods),
"note" => Some(Self::Note),
"notes" => Some(Self::Notes),
"other args" => Some(Self::OtherArgs),
"other arguments" => Some(Self::OtherArguments),
"other params" => Some(Self::OtherParams),
"other parameters" => Some(Self::OtherParameters),
"parameters" => Some(Self::Parameters),
"raises" => Some(Self::Raises),
@@ -97,6 +108,9 @@ impl SectionKind {
Self::Methods => "Methods",
Self::Note => "Note",
Self::Notes => "Notes",
Self::OtherArgs => "Other Args",
Self::OtherArguments => "Other Arguments",
Self::OtherParams => "Other Params",
Self::OtherParameters => "Other Parameters",
Self::Parameters => "Parameters",
Self::Raises => "Raises",
@@ -116,17 +130,259 @@ impl SectionKind {
}
}
pub(crate) struct SectionContexts<'a> {
contexts: Vec<SectionContextData>,
docstring: &'a Docstring<'a>,
}
impl<'a> SectionContexts<'a> {
/// Extract all `SectionContext` values from a docstring.
pub fn from_docstring(docstring: &'a Docstring<'a>, style: SectionStyle) -> Self {
let contents = docstring.body();
let mut contexts = Vec::new();
let mut last: Option<SectionContextData> = None;
let mut previous_line = None;
for line in contents.universal_newlines() {
if previous_line.is_none() {
// skip the first line
previous_line = Some(line.as_str());
continue;
}
if let Some(section_kind) = suspected_as_section(&line, style) {
let indent = whitespace::leading_space(&line);
let section_name = whitespace::leading_words(&line);
let section_name_range = TextRange::at(indent.text_len(), section_name.text_len());
if is_docstring_section(
&line,
section_name_range,
previous_line.unwrap_or_default(),
) {
if let Some(mut last) = last.take() {
last.range = TextRange::new(last.range.start(), line.start());
contexts.push(last);
}
last = Some(SectionContextData {
kind: section_kind,
name_range: section_name_range + line.start(),
range: TextRange::empty(line.start()),
summary_full_end: line.full_end(),
});
}
}
previous_line = Some(line.as_str());
}
if let Some(mut last) = last.take() {
last.range = TextRange::new(last.range.start(), contents.text_len());
contexts.push(last);
}
Self {
contexts,
docstring,
}
}
pub fn len(&self) -> usize {
self.contexts.len()
}
pub fn iter(&self) -> SectionContextsIter {
SectionContextsIter {
docstring_body: self.docstring.body(),
inner: self.contexts.iter(),
}
}
}
impl<'a> IntoIterator for &'a SectionContexts<'a> {
type Item = SectionContext<'a>;
type IntoIter = SectionContextsIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl Debug for SectionContexts<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_list().entries(self.iter()).finish()
}
}
pub struct SectionContextsIter<'a> {
docstring_body: DocstringBody<'a>,
inner: std::slice::Iter<'a, SectionContextData>,
}
impl<'a> Iterator for SectionContextsIter<'a> {
type Item = SectionContext<'a>;
fn next(&mut self) -> Option<Self::Item> {
let next = self.inner.next()?;
Some(SectionContext {
data: next,
docstring_body: self.docstring_body,
})
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<'a> DoubleEndedIterator for SectionContextsIter<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
let back = self.inner.next_back()?;
Some(SectionContext {
data: back,
docstring_body: self.docstring_body,
})
}
}
impl FusedIterator for SectionContextsIter<'_> {}
impl ExactSizeIterator for SectionContextsIter<'_> {}
#[derive(Debug)]
pub(crate) struct SectionContext<'a> {
/// The "kind" of the section, e.g. "SectionKind::Args" or "SectionKind::Returns".
pub(crate) kind: SectionKind,
struct SectionContextData {
kind: SectionKind,
/// Range of the section name, relative to the [`Docstring::body`]
name_range: TextRange,
/// Range from the start to the end of the section, relative to the [`Docstring::body`]
range: TextRange,
/// End of the summary, relative to the [`Docstring::body`]
summary_full_end: TextSize,
}
pub struct SectionContext<'a> {
data: &'a SectionContextData,
docstring_body: DocstringBody<'a>,
}
impl<'a> SectionContext<'a> {
pub fn is_last(&self) -> bool {
self.range().end() == self.docstring_body.end()
}
/// The `kind` of the section, e.g. [`SectionKind::Args`] or [`SectionKind::Returns`].
pub const fn kind(&self) -> SectionKind {
self.data.kind
}
/// The name of the section as it appears in the docstring, e.g. "Args" or "Returns".
pub(crate) section_name: &'a str,
pub(crate) previous_line: &'a str,
pub(crate) line: &'a str,
pub(crate) following_lines: &'a [&'a str],
pub(crate) is_last_section: bool,
pub(crate) original_index: usize,
pub fn section_name(&self) -> &'a str {
&self.docstring_body.as_str()[self.data.name_range]
}
/// Returns the rest of the summary line after the section name.
pub fn summary_after_section_name(&self) -> &'a str {
&self.summary_line()[usize::from(self.data.name_range.end() - self.data.range.start())..]
}
fn offset(&self) -> TextSize {
self.docstring_body.start()
}
/// The absolute range of the section name
pub fn section_name_range(&self) -> TextRange {
self.data.name_range + self.offset()
}
/// Summary range relative to the start of the document. Includes the trailing newline.
pub fn summary_full_range(&self) -> TextRange {
self.summary_full_range_relative() + self.offset()
}
/// The absolute range of the summary line, excluding any trailing newline character.
pub fn summary_range(&self) -> TextRange {
TextRange::at(self.range().start(), self.summary_line().text_len())
}
/// Range of the summary line relative to [`Docstring::body`], including the trailing newline character.
fn summary_full_range_relative(&self) -> TextRange {
TextRange::new(self.range_relative().start(), self.data.summary_full_end)
}
/// Returns the range of this section relative to [`Docstring::body`]
const fn range_relative(&self) -> TextRange {
self.data.range
}
/// The absolute range of the full-section.
pub fn range(&self) -> TextRange {
self.range_relative() + self.offset()
}
/// Summary line without the trailing newline characters
pub fn summary_line(&self) -> &'a str {
let full_summary = &self.docstring_body.as_str()[self.summary_full_range_relative()];
let mut bytes = full_summary.bytes().rev();
let newline_width = match bytes.next() {
Some(b'\n') => {
if bytes.next() == Some(b'\r') {
2
} else {
1
}
}
Some(b'\r') => 1,
_ => 0,
};
&full_summary[..full_summary.len() - newline_width]
}
/// Returns the text of the last line of the previous section or an empty string if it is the first section.
pub fn previous_line(&self) -> Option<&'a str> {
let previous =
&self.docstring_body.as_str()[TextRange::up_to(self.range_relative().start())];
previous.universal_newlines().last().map(|l| l.as_str())
}
/// Returns the lines belonging to this section after the summary line.
pub fn following_lines(&self) -> UniversalNewlineIterator<'a> {
let lines = self.following_lines_str();
UniversalNewlineIterator::with_offset(lines, self.offset() + self.data.summary_full_end)
}
fn following_lines_str(&self) -> &'a str {
&self.docstring_body.as_str()[self.following_range_relative()]
}
/// Returns the range to the following lines relative to [`Docstring::body`].
const fn following_range_relative(&self) -> TextRange {
TextRange::new(self.data.summary_full_end, self.range_relative().end())
}
/// Returns the absolute range of the following lines.
pub fn following_range(&self) -> TextRange {
self.following_range_relative() + self.offset()
}
}
impl Debug for SectionContext<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SectionContext")
.field("kind", &self.kind())
.field("section_name", &self.section_name())
.field("summary_line", &self.summary_line())
.field("following_lines", &&self.following_lines_str())
.finish()
}
}
fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind> {
@@ -139,20 +395,15 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind>
}
/// Check if the suspected context is really a section header.
fn is_docstring_section(context: &SectionContext) -> bool {
let section_name_suffix = context
.line
.trim()
.strip_prefix(context.section_name)
.unwrap()
.trim();
fn is_docstring_section(line: &str, section_name_range: TextRange, previous_lines: &str) -> bool {
let section_name_suffix = line[usize::from(section_name_range.end())..].trim();
let this_looks_like_a_section_name =
section_name_suffix == ":" || section_name_suffix.is_empty();
if !this_looks_like_a_section_name {
return false;
}
let prev_line = context.previous_line.trim();
let prev_line = previous_lines.trim();
let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
.into_iter()
.any(|char| prev_line.ends_with(char));
@@ -164,50 +415,3 @@ fn is_docstring_section(context: &SectionContext) -> bool {
true
}
/// Extract all `SectionContext` values from a docstring.
pub(crate) fn section_contexts<'a>(
lines: &'a [&'a str],
style: SectionStyle,
) -> Vec<SectionContext<'a>> {
let mut contexts = vec![];
for (kind, lineno) in lines
.iter()
.enumerate()
.skip(1)
.filter_map(|(lineno, line)| suspected_as_section(line, style).map(|kind| (kind, lineno)))
{
let context = SectionContext {
kind,
section_name: whitespace::leading_words(lines[lineno]),
previous_line: lines[lineno - 1],
line: lines[lineno],
following_lines: &lines[lineno + 1..],
original_index: lineno,
is_last_section: false,
};
if is_docstring_section(&context) {
contexts.push(context);
}
}
let mut truncated_contexts = Vec::with_capacity(contexts.len());
let mut end: Option<usize> = None;
for context in contexts.into_iter().rev() {
let next_end = context.original_index;
truncated_contexts.push(SectionContext {
kind: context.kind,
section_name: context.section_name,
previous_line: context.previous_line,
line: context.line,
following_lines: end.map_or(context.following_lines, |end| {
&lines[context.original_index + 1..end]
}),
original_index: context.original_index,
is_last_section: end.is_none(),
});
end = Some(next_end);
}
truncated_contexts.reverse();
truncated_contexts
}

View File

@@ -2,8 +2,9 @@
use anyhow::Result;
use libcst_native::{Codegen, CodegenState, ImportAlias, Name, NameOrAttribute};
use ruff_text_size::TextSize;
use rustc_hash::FxHashMap;
use rustpython_parser::ast::{Location, Stmt, StmtKind, Suite};
use rustpython_parser::ast::{Stmt, StmtKind, Suite};
use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::Edit;
@@ -95,7 +96,7 @@ impl<'a> Importer<'a> {
/// Add the given member to an existing `StmtKind::ImportFrom` statement.
pub fn add_member(&self, stmt: &Stmt, member: &str) -> Result<Edit> {
let mut tree = match_module(self.locator.slice(stmt))?;
let mut tree = match_module(self.locator.slice(stmt.range()))?;
let import_from = match_import_from(&mut tree)?;
let aliases = match_aliases(import_from)?;
aliases.push(ImportAlias {
@@ -113,11 +114,7 @@ impl<'a> Importer<'a> {
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Edit::replacement(
state.to_string(),
stmt.location,
stmt.end_location.unwrap(),
))
Ok(Edit::range_replacement(state.to_string(), stmt.range()))
}
}
@@ -126,13 +123,13 @@ struct Insertion {
/// The content to add before the insertion.
prefix: &'static str,
/// The location at which to insert.
location: Location,
location: TextSize,
/// The content to add after the insertion.
suffix: &'static str,
}
impl Insertion {
fn new(prefix: &'static str, location: Location, suffix: &'static str) -> Self {
fn new(prefix: &'static str, location: TextSize, suffix: &'static str) -> Self {
Self {
prefix,
location,
@@ -142,7 +139,7 @@ impl Insertion {
}
/// Find the end of the last docstring.
fn match_docstring_end(body: &[Stmt]) -> Option<Location> {
fn match_docstring_end(body: &[Stmt]) -> Option<TextSize> {
let mut iter = body.iter();
let Some(mut stmt) = iter.next() else {
return None;
@@ -156,10 +153,10 @@ fn match_docstring_end(body: &[Stmt]) -> Option<Location> {
}
stmt = next;
}
Some(stmt.end_location.unwrap())
Some(stmt.end())
}
/// Find the location at which a "top-of-file" import should be inserted,
/// Find the location at which an "end-of-statement" import should be inserted,
/// along with a prefix and suffix to use for the insertion.
///
/// For example, given the following code:
@@ -168,22 +165,28 @@ fn match_docstring_end(body: &[Stmt]) -> Option<Location> {
/// """Hello, world!"""
///
/// import os
/// import math
///
///
/// def foo():
/// pass
/// ```
///
/// The location returned will be the start of the `import os` statement,
/// The location returned will be the start of new line after the last
/// import statement, which in this case is the line after `import math`,
/// along with a trailing newline suffix.
fn end_of_statement_insertion(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> Insertion {
let location = stmt.end_location.unwrap();
let location = stmt.end();
let mut tokens = lexer::lex_located(locator.after(location), Mode::Module, location).flatten();
if let Some((.., Tok::Semi, end)) = tokens.next() {
if let Some((Tok::Semi, range)) = tokens.next() {
// If the first token after the docstring is a semicolon, insert after the semicolon as an
// inline statement;
Insertion::new(" ", end, ";")
Insertion::new(" ", range.end(), ";")
} else {
// Otherwise, insert on the next line.
Insertion::new(
"",
Location::new(location.row() + 1, 0),
locator.full_line_end(location),
stylist.line_ending().as_str(),
)
}
@@ -210,22 +213,22 @@ fn top_of_file_insertion(body: &[Stmt], locator: &Locator, stylist: &Stylist) ->
let first_token = lexer::lex_located(locator.after(location), Mode::Module, location)
.flatten()
.next();
if let Some((.., Tok::Semi, end)) = first_token {
return Insertion::new(" ", end, ";");
if let Some((Tok::Semi, range)) = first_token {
return Insertion::new(" ", range.end(), ";");
}
// Otherwise, advance to the next row.
Location::new(location.row() + 1, 0)
locator.full_line_end(location)
} else {
Location::default()
TextSize::default()
};
// Skip over any comments and empty lines.
for (.., tok, end) in
for (tok, range) in
lexer::lex_located(locator.after(location), Mode::Module, location).flatten()
{
if matches!(tok, Tok::Comment(..) | Tok::Newline) {
location = Location::new(end.row() + 1, 0);
location = locator.full_line_end(range.end());
} else {
break;
}
@@ -237,11 +240,12 @@ fn top_of_file_insertion(body: &[Stmt], locator: &Locator, stylist: &Stylist) ->
#[cfg(test)]
mod tests {
use anyhow::Result;
use ruff_python_ast::newlines::LineEnding;
use ruff_text_size::TextSize;
use rustpython_parser as parser;
use rustpython_parser::ast::Location;
use rustpython_parser::lexer::LexResult;
use ruff_python_ast::source_code::{LineEnding, Locator, Stylist};
use ruff_python_ast::source_code::{Locator, Stylist};
use crate::importer::{top_of_file_insertion, Insertion};
@@ -258,7 +262,7 @@ mod tests {
let contents = "";
assert_eq!(
insert(contents)?,
Insertion::new("", Location::new(1, 0), LineEnding::default().as_str())
Insertion::new("", TextSize::from(0), LineEnding::default().as_str())
);
let contents = r#"
@@ -266,7 +270,7 @@ mod tests {
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::new("", Location::new(2, 0), LineEnding::default().as_str())
Insertion::new("", TextSize::from(19), LineEnding::default().as_str())
);
let contents = r#"
@@ -275,7 +279,7 @@ mod tests {
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::new("", Location::new(2, 0), "\n")
Insertion::new("", TextSize::from(20), "\n")
);
let contents = r#"
@@ -285,7 +289,7 @@ mod tests {
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::new("", Location::new(3, 0), "\n")
Insertion::new("", TextSize::from(40), "\n")
);
let contents = r#"
@@ -294,7 +298,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::new("", Location::new(1, 0), "\n")
Insertion::new("", TextSize::from(0), "\n")
);
let contents = r#"
@@ -303,7 +307,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::new("", Location::new(2, 0), "\n")
Insertion::new("", TextSize::from(23), "\n")
);
let contents = r#"
@@ -313,7 +317,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::new("", Location::new(3, 0), "\n")
Insertion::new("", TextSize::from(43), "\n")
);
let contents = r#"
@@ -323,7 +327,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::new("", Location::new(3, 0), "\n")
Insertion::new("", TextSize::from(43), "\n")
);
let contents = r#"
@@ -332,7 +336,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::new("", Location::new(1, 0), "\n")
Insertion::new("", TextSize::from(0), "\n")
);
let contents = r#"
@@ -341,7 +345,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::new(" ", Location::new(1, 20), ";")
Insertion::new(" ", TextSize::from(20), ";")
);
let contents = r#"
@@ -351,7 +355,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::new(" ", Location::new(1, 20), ";")
Insertion::new(" ", TextSize::from(20), ";")
);
Ok(())

View File

@@ -1,3 +1,4 @@
use ruff_text_size::TextRange;
use std::fs::File;
use std::io::{BufReader, BufWriter};
use std::iter;
@@ -7,7 +8,6 @@ use serde::Serialize;
use serde_json::error::Category;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::types::Range;
use crate::jupyter::{CellType, JupyterNotebook, SourceValue};
use crate::rules::pycodestyle::rules::SyntaxError;
@@ -18,7 +18,7 @@ pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb";
/// Jupyter Notebook indexing table
///
/// When we lint a jupyter notebook, we have to translate the row/column based on
/// [`crate::message::Location`]
/// [`ruff_text_size::TextSize`]
/// to jupyter notebook cell/row/column.
#[derive(Debug, Eq, PartialEq)]
pub struct JupyterIndex {
@@ -46,7 +46,7 @@ impl JupyterNotebook {
IOError {
message: format!("{err}"),
},
Range::default(),
TextRange::default(),
)
})?);
let notebook: JupyterNotebook = match serde_json::from_reader(reader) {
@@ -59,7 +59,7 @@ impl JupyterNotebook {
IOError {
message: format!("{err}"),
},
Range::default(),
TextRange::default(),
),
Category::Syntax | Category::Eof => {
// Maybe someone saved the python sources (those with the `# %%` separator)
@@ -69,7 +69,7 @@ impl JupyterNotebook {
IOError {
message: format!("{err}"),
},
Range::default(),
TextRange::default(),
)
})?;
// Check if tokenizing was successful and the file is non-empty
@@ -84,7 +84,7 @@ impl JupyterNotebook {
but this file isn't valid JSON: {err}"
),
},
Range::default(),
TextRange::default(),
)
} else {
Diagnostic::new(
@@ -95,7 +95,7 @@ impl JupyterNotebook {
but found a Python source file: {err}"
),
},
Range::default(),
TextRange::default(),
)
}
}
@@ -108,7 +108,7 @@ impl JupyterNotebook {
"This file does not match the schema expected of Jupyter Notebooks: {err}"
),
},
Range::default(),
TextRange::default(),
)
}
}
@@ -126,7 +126,7 @@ impl JupyterNotebook {
notebook.nbformat
),
},
Range::default(),
TextRange::default(),
)));
}

View File

@@ -6,7 +6,6 @@
//! [Ruff]: https://github.com/charliermarsh/ruff
pub use ruff_python_ast::source_code::round_trip;
pub use ruff_python_ast::types::Range;
pub use rule_selector::RuleSelector;
pub use rules::pycodestyle::rules::IOError;

View File

@@ -4,6 +4,7 @@ use std::path::Path;
use anyhow::{anyhow, Result};
use colored::Colorize;
use itertools::Itertools;
use log::error;
use rustc_hash::FxHashMap;
use rustpython_parser::lexer::LexResult;
@@ -23,6 +24,7 @@ use crate::checkers::physical_lines::check_physical_lines;
use crate::checkers::tokens::check_tokens;
use crate::directives::Directives;
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
use crate::logging::DisplayParseError;
use crate::message::Message;
use crate::noqa::add_noqa;
use crate::registry::{AsRule, Rule};
@@ -68,7 +70,6 @@ pub struct FixerResult<'a> {
pub fn check_path(
path: &Path,
package: Option<&Path>,
contents: &str,
tokens: Vec<LexResult>,
locator: &Locator,
stylist: &Stylist,
@@ -88,7 +89,7 @@ pub fn check_path(
let use_doc_lines = settings.rules.enabled(Rule::DocLineTooLong);
let mut doc_lines = vec![];
if use_doc_lines {
doc_lines.extend(doc_lines_from_tokens(&tokens));
doc_lines.extend(doc_lines_from_tokens(&tokens, locator));
}
// Run the token-based rules.
@@ -178,7 +179,7 @@ pub fn check_path(
// if it's disabled via any of the usual mechanisms (e.g., `noqa`,
// `per-file-ignores`), and the easiest way to detect that suppression is
// to see if the diagnostic persists to the end of the function.
pycodestyle::rules::syntax_error(&mut diagnostics, &parse_error);
pycodestyle::rules::syntax_error(&mut diagnostics, &parse_error, locator);
error = Some(parse_error);
}
}
@@ -218,8 +219,8 @@ pub fn check_path(
{
let ignored = check_noqa(
&mut diagnostics,
contents,
indexer.commented_lines(),
locator,
indexer.comment_ranges(),
&directives.noqa_line_for,
settings,
error.as_ref().map_or(autofix, |_| flags::Autofix::Disabled),
@@ -268,11 +269,15 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings
let stylist = Stylist::from_tokens(&tokens, &locator);
// Extra indices from the code.
let indexer: Indexer = tokens.as_slice().into();
let indexer = Indexer::from_tokens(&tokens, &locator);
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(settings));
let directives = directives::extract_directives(
&tokens,
directives::Flags::from_settings(settings),
&locator,
&indexer,
);
// Generate diagnostics, ignoring any existing `noqa` directives.
let LinterResult {
@@ -281,7 +286,6 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings
} = check_path(
path,
package,
&contents,
tokens,
&locator,
&stylist,
@@ -294,20 +298,15 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings
// Log any parse errors.
if let Some(err) = error {
error!(
"{}{}{} {err:?}",
"Failed to parse ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);
error!("{}", DisplayParseError::new(err, locator.to_source_code()));
}
// Add any missing `# noqa` pragmas.
add_noqa(
path,
&diagnostics.0,
&contents,
indexer.commented_lines(),
&locator,
indexer.comment_ranges(),
&directives.noqa_line_for,
stylist.line_ending(),
)
@@ -333,17 +332,20 @@ pub fn lint_only(
let stylist = Stylist::from_tokens(&tokens, &locator);
// Extra indices from the code.
let indexer: Indexer = tokens.as_slice().into();
let indexer = Indexer::from_tokens(&tokens, &locator);
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(settings));
let directives = directives::extract_directives(
&tokens,
directives::Flags::from_settings(settings),
&locator,
&indexer,
);
// Generate diagnostics.
let result = check_path(
path,
package,
contents,
tokens,
&locator,
&stylist,
@@ -356,7 +358,7 @@ pub fn lint_only(
result.map(|(diagnostics, imports)| {
(
diagnostics_to_messages(diagnostics, path, settings, &locator, &directives),
diagnostics_to_messages(diagnostics, path, &locator, &directives),
imports,
)
})
@@ -366,14 +368,15 @@ pub fn lint_only(
fn diagnostics_to_messages(
diagnostics: Vec<Diagnostic>,
path: &Path,
settings: &Settings,
locator: &Locator,
directives: &Directives,
) -> Vec<Message> {
let file = once_cell::unsync::Lazy::new(|| {
let mut builder = SourceFileBuilder::new(&path.to_string_lossy());
if settings.show_source {
builder.set_source_code(&locator.to_source_code());
let mut builder =
SourceFileBuilder::new(path.to_string_lossy().as_ref(), locator.contents());
if let Some(line_index) = locator.line_index() {
builder.set_line_index(line_index.clone());
}
builder.finish()
@@ -382,9 +385,8 @@ fn diagnostics_to_messages(
diagnostics
.into_iter()
.map(|diagnostic| {
let lineno = diagnostic.location.row();
let noqa_row = *directives.noqa_line_for.get(&lineno).unwrap_or(&lineno);
Message::from_diagnostic(diagnostic, file.deref().clone(), noqa_row)
let noqa_offset = directives.noqa_line_for.resolve(diagnostic.start());
Message::from_diagnostic(diagnostic, file.deref().clone(), noqa_offset)
})
.collect()
}
@@ -421,17 +423,20 @@ pub fn lint_fix<'a>(
let stylist = Stylist::from_tokens(&tokens, &locator);
// Extra indices from the code.
let indexer: Indexer = tokens.as_slice().into();
let indexer = Indexer::from_tokens(&tokens, &locator);
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(settings));
let directives = directives::extract_directives(
&tokens,
directives::Flags::from_settings(settings),
&locator,
&indexer,
);
// Generate diagnostics.
let result = check_path(
path,
package,
&transformed,
tokens,
&locator,
&stylist,
@@ -449,24 +454,12 @@ pub fn lint_fix<'a>(
// longer parseable on a subsequent pass, then we've introduced a
// syntax error. Return the original code.
if parseable && result.error.is_some() {
#[allow(clippy::print_stderr)]
{
eprintln!(
r#"
{}: Autofix introduced a syntax error. Reverting all changes.
This indicates a bug in `{}`. If you could open an issue at:
{}/issues/new?title=%5BAutofix%20error%5D
...quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
"#,
"error".red().bold(),
CARGO_PKG_NAME,
CARGO_PKG_REPOSITORY,
fs::relativize_path(path),
);
}
report_autofix_syntax_error(
path,
&transformed,
&result.error.unwrap(),
fixed.keys().copied(),
);
return Err(anyhow!("Autofix introduced a syntax error"));
}
}
@@ -489,31 +482,13 @@ This indicates a bug in `{}`. If you could open an issue at:
continue;
}
#[allow(clippy::print_stderr)]
{
eprintln!(
r#"
{}: Failed to converge after {} iterations.
This indicates a bug in `{}`. If you could open an issue at:
{}/issues/new?title=%5BInfinite%20loop%5D
...quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
"#,
"error".red().bold(),
MAX_ITERATIONS,
CARGO_PKG_NAME,
CARGO_PKG_REPOSITORY,
fs::relativize_path(path),
);
}
report_failed_to_converge_error(path, &transformed, &result.data.0);
}
return Ok(FixerResult {
result: result.map(|(diagnostics, imports)| {
(
diagnostics_to_messages(diagnostics, path, settings, &locator, &directives),
diagnostics_to_messages(diagnostics, path, &locator, &directives),
imports,
)
}),
@@ -522,3 +497,80 @@ This indicates a bug in `{}`. If you could open an issue at:
});
}
}
fn collect_rule_codes(rules: impl IntoIterator<Item = Rule>) -> String {
rules
.into_iter()
.map(|rule| rule.noqa_code().to_string())
.sorted_unstable()
.dedup()
.join(", ")
}
#[allow(clippy::print_stderr)]
fn report_failed_to_converge_error(path: &Path, transformed: &str, diagnostics: &[Diagnostic]) {
if cfg!(debug_assertions) {
let codes = collect_rule_codes(diagnostics.iter().map(|diagnostic| diagnostic.kind.rule()));
eprintln!(
"{}: Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---",
"debug error".red().bold(),
MAX_ITERATIONS,
fs::relativize_path(path),
codes,
transformed,
);
} else {
eprintln!(
r#"
{}: Failed to converge after {} iterations.
This indicates a bug in `{}`. If you could open an issue at:
{}/issues/new?title=%5BInfinite%20loop%5D
...quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
"#,
"error".red().bold(),
MAX_ITERATIONS,
CARGO_PKG_NAME,
CARGO_PKG_REPOSITORY,
fs::relativize_path(path),
);
}
}
#[allow(clippy::print_stderr)]
fn report_autofix_syntax_error(
path: &Path,
transformed: &str,
error: &ParseError,
rules: impl IntoIterator<Item = Rule>,
) {
if cfg!(debug_assertions) {
let codes = collect_rule_codes(rules);
eprintln!(
"{}: Autofix introduced a syntax error in `{}` with rule codes {}: {}\n---\n{}\n---",
"error".red().bold(),
fs::relativize_path(path),
codes,
error,
transformed,
);
} else {
eprintln!(
r#"
{}: Autofix introduced a syntax error. Reverting all changes.
This indicates a bug in `{}`. If you could open an issue at:
{}/issues/new?title=%5BAutofix%20error%5D
...quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
"#,
"error".red().bold(),
CARGO_PKG_NAME,
CARGO_PKG_REPOSITORY,
fs::relativize_path(path),
);
}
}

View File

@@ -1,10 +1,15 @@
use std::fmt::{Display, Formatter};
use std::path::Path;
use std::sync::Mutex;
use crate::fs;
use anyhow::Result;
use colored::Colorize;
use fern;
use log::Level;
use once_cell::sync::Lazy;
use ruff_python_ast::source_code::SourceCode;
use rustpython_parser::ParseError;
pub(crate) static WARNINGS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::default);
@@ -42,13 +47,13 @@ macro_rules! warn_user_once {
#[macro_export]
macro_rules! warn_user {
($($arg:tt)*) => {
($($arg:tt)*) => {{
use colored::Colorize;
use log::warn;
let message = format!("{}", format_args!($($arg)*));
warn!("{}", message.bold());
};
}};
}
#[macro_export]
@@ -127,6 +132,34 @@ pub fn set_up_logging(level: &LogLevel) -> Result<()> {
Ok(())
}
pub struct DisplayParseError<'a> {
error: ParseError,
source_code: SourceCode<'a, 'a>,
}
impl<'a> DisplayParseError<'a> {
pub fn new(error: ParseError, source_code: SourceCode<'a, 'a>) -> Self {
Self { error, source_code }
}
}
impl Display for DisplayParseError<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let source_location = self.source_code.source_location(self.error.location);
write!(
f,
"{header} {path}{colon}{row}{colon}{column}{colon} {inner}",
header = "Failed to parse ".bold(),
path = fs::relativize_path(Path::new(&self.error.source_path)).bold(),
row = source_location.row,
column = source_location.column,
colon = ":".cyan(),
inner = &self.error.error
)
}
}
#[cfg(test)]
mod tests {
use crate::logging::LogLevel;

View File

@@ -1,5 +1,6 @@
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_python_ast::source_code::{OneIndexed, SourceLocation};
use std::io::Write;
/// Generate error logging commands for Azure Pipelines format.
@@ -15,12 +16,15 @@ impl Emitter for AzureEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for message in messages {
let (line, col) = if context.is_jupyter_notebook(message.filename()) {
let location = if context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
(1, 0)
SourceLocation {
row: OneIndexed::from_zero_indexed(0),
column: OneIndexed::from_zero_indexed(0),
}
} else {
(message.location.row(), message.location.column())
message.compute_start_location()
};
writeln!(
@@ -28,6 +32,8 @@ impl Emitter for AzureEmitter {
"##vso[task.logissue type=error\
;sourcepath={filename};linenumber={line};columnnumber={col};code={code};]{body}",
filename = message.filename(),
line = location.row,
col = location.column,
code = message.kind.rule().noqa_code(),
body = message.kind.body,
)?;

View File

@@ -1,8 +1,7 @@
use crate::message::Message;
use colored::{Color, ColoredString, Colorize, Styles};
use ruff_diagnostics::Fix;
use ruff_python_ast::source_code::{OneIndexed, SourceCode};
use ruff_python_ast::types::Range;
use ruff_python_ast::source_code::{OneIndexed, SourceFile};
use ruff_text_size::{TextRange, TextSize};
use similar::{ChangeTag, TextDiff};
use std::fmt::{Display, Formatter};
@@ -18,38 +17,39 @@ use std::num::NonZeroUsize;
/// * Compute the diff from the [`Edit`] because diff calculation is expensive.
pub(super) struct Diff<'a> {
fix: &'a Fix,
source_code: SourceCode<'a, 'a>,
source_code: &'a SourceFile,
}
impl<'a> Diff<'a> {
pub fn from_message(message: &'a Message) -> Option<Diff> {
match message.file.source_code() {
Some(source_code) if !message.fix.is_empty() => Some(Diff {
source_code,
if message.fix.is_empty() {
None
} else {
Some(Diff {
source_code: &message.file,
fix: &message.fix,
}),
_ => None,
})
}
}
}
impl Display for Diff<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut output = String::with_capacity(self.source_code.text().len());
let mut output = String::with_capacity(self.source_code.source_text().len());
let mut last_end = TextSize::default();
for edit in self.fix.edits() {
let edit_range = self
.source_code
.text_range(Range::new(edit.location(), edit.end_location()));
output.push_str(&self.source_code.text()[TextRange::new(last_end, edit_range.start())]);
output.push_str(
self.source_code
.slice(TextRange::new(last_end, edit.start())),
);
output.push_str(edit.content().unwrap_or_default());
last_end = edit_range.end();
last_end = edit.end();
}
output.push_str(&self.source_code.text()[usize::from(last_end)..]);
output.push_str(&self.source_code.source_text()[usize::from(last_end)..]);
let diff = TextDiff::from_lines(self.source_code.text(), &output);
let diff = TextDiff::from_lines(self.source_code.source_text(), &output);
writeln!(f, "{}", " Suggested fix".blue())?;

View File

@@ -1,6 +1,7 @@
use crate::fs::relativize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_python_ast::source_code::{OneIndexed, SourceLocation};
use std::io::Write;
/// Generate error workflow command in GitHub Actions format.
@@ -16,30 +17,38 @@ impl Emitter for GithubEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for message in messages {
let (row, column) = if context.is_jupyter_notebook(message.filename()) {
let source_location = message.compute_start_location();
let location = if context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
(1, 0)
SourceLocation {
row: OneIndexed::from_zero_indexed(0),
column: OneIndexed::from_zero_indexed(0),
}
} else {
(message.location.row(), message.location.column())
source_location.clone()
};
let end_location = message.compute_end_location();
write!(
writer,
"::error title=Ruff \
({code}),file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::",
code = message.kind.rule().noqa_code(),
file = message.filename(),
row = message.location.row(),
column = message.location.column(),
end_row = message.end_location.row(),
end_column = message.end_location.column(),
row = source_location.row,
column = source_location.column,
end_row = end_location.row,
end_column = end_location.column,
)?;
writeln!(
writer,
"{path}:{row}:{column}: {code} {body}",
path = relativize_path(message.filename()),
row = location.row,
column = location.column,
code = message.kind.rule().noqa_code(),
body = message.kind.body,
)?;

View File

@@ -1,6 +1,7 @@
use crate::fs::{relativize_path, relativize_path_to};
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_python_ast::source_code::SourceLocation;
use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use serde_json::json;
@@ -56,6 +57,9 @@ impl Serialize for SerializedMessages<'_> {
let mut s = serializer.serialize_seq(Some(self.messages.len()))?;
for message in self.messages {
let start_location = message.compute_start_location();
let end_location = message.compute_end_location();
let lines = if self.context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
@@ -65,8 +69,8 @@ impl Serialize for SerializedMessages<'_> {
})
} else {
json!({
"begin": message.location.row(),
"end": message.end_location.row()
"begin": start_location.row,
"end": end_location.row
})
};
@@ -78,7 +82,7 @@ impl Serialize for SerializedMessages<'_> {
let value = json!({
"description": format!("({}) {}", message.kind.rule().noqa_code(), message.kind.body),
"severity": "major",
"fingerprint": fingerprint(message),
"fingerprint": fingerprint(message, &start_location, &end_location),
"location": {
"path": path,
"lines": lines
@@ -93,23 +97,24 @@ impl Serialize for SerializedMessages<'_> {
}
/// Generate a unique fingerprint to identify a violation.
fn fingerprint(message: &Message) -> String {
fn fingerprint(
message: &Message,
start_location: &SourceLocation,
end_location: &SourceLocation,
) -> String {
let Message {
kind,
location,
end_location,
range: _,
fix: _fix,
file,
noqa_row: _noqa_row,
noqa_offset: _,
} = message;
let mut hasher = DefaultHasher::new();
kind.rule().hash(&mut hasher);
location.row().hash(&mut hasher);
location.column().hash(&mut hasher);
end_location.row().hash(&mut hasher);
end_location.column().hash(&mut hasher);
start_location.hash(&mut hasher);
end_location.hash(&mut hasher);
file.name().hash(&mut hasher);
format!("{:x}", hasher.finish())

View File

@@ -1,14 +1,20 @@
use crate::fs::relativize_path;
use crate::jupyter::JupyterIndex;
use crate::message::diff::calculate_print_width;
use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
use crate::message::{group_messages_by_filename, Emitter, EmitterContext, Message};
use crate::message::{
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
};
use colored::Colorize;
use ruff_python_ast::source_code::OneIndexed;
use std::fmt::{Display, Formatter};
use std::io::Write;
use std::num::NonZeroUsize;
#[derive(Default)]
pub struct GroupedEmitter {
show_fix_status: bool,
show_source: bool,
}
impl GroupedEmitter {
@@ -17,6 +23,12 @@ impl GroupedEmitter {
self.show_fix_status = show_fix_status;
self
}
#[must_use]
pub fn with_show_source(mut self, show_source: bool) -> Self {
self.show_source = show_source;
self
}
}
impl Emitter for GroupedEmitter {
@@ -29,20 +41,17 @@ impl Emitter for GroupedEmitter {
for (filename, messages) in group_messages_by_filename(messages) {
// Compute the maximum number of digits in the row and column, for messages in
// this file.
let row_length = num_digits(
messages
.iter()
.map(|message| message.location.row())
.max()
.unwrap(),
);
let column_length = num_digits(
messages
.iter()
.map(|message| message.location.column())
.max()
.unwrap(),
);
let mut max_row_length = OneIndexed::MIN;
let mut max_column_length = OneIndexed::MIN;
for message in &messages {
max_row_length = max_row_length.max(message.start_location.row);
max_column_length = max_column_length.max(message.start_location.column);
}
let row_length = calculate_print_width(max_row_length);
let column_length = calculate_print_width(max_column_length);
// Print the filename.
writeln!(writer, "{}:", relativize_path(filename).underline())?;
@@ -53,11 +62,12 @@ impl Emitter for GroupedEmitter {
writer,
"{}",
DisplayGroupedMessage {
jupyter_index: context.jupyter_index(message.filename()),
message,
show_fix_status: self.show_fix_status,
show_source: self.show_source,
row_length,
column_length,
jupyter_index: context.jupyter_index(message.filename()),
}
)?;
}
@@ -69,21 +79,26 @@ impl Emitter for GroupedEmitter {
}
struct DisplayGroupedMessage<'a> {
message: &'a Message,
message: MessageWithLocation<'a>,
show_fix_status: bool,
row_length: usize,
column_length: usize,
show_source: bool,
row_length: NonZeroUsize,
column_length: NonZeroUsize,
jupyter_index: Option<&'a JupyterIndex>,
}
impl Display for DisplayGroupedMessage<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let message = self.message;
let MessageWithLocation {
message,
start_location,
} = &self.message;
write!(
f,
" {row_padding}",
row_padding = " ".repeat(self.row_length - num_digits(message.location.row()))
row_padding =
" ".repeat(self.row_length.get() - calculate_print_width(start_location.row).get())
)?;
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
@@ -91,29 +106,31 @@ impl Display for DisplayGroupedMessage<'_> {
write!(
f,
"cell {cell}{sep}",
cell = jupyter_index.row_to_cell[message.location.row()],
cell = jupyter_index.row_to_cell[start_location.row.get()],
sep = ":".cyan()
)?;
(
jupyter_index.row_to_row_in_cell[message.location.row()] as usize,
message.location.column(),
jupyter_index.row_to_row_in_cell[start_location.row.get()] as usize,
start_location.column.get(),
)
} else {
(message.location.row(), message.location.column())
(start_location.row.get(), start_location.column.get())
};
writeln!(
f,
"{row}{sep}{col}{col_padding} {code_and_body}",
sep = ":".cyan(),
col_padding = " ".repeat(self.column_length - num_digits(message.location.column())),
col_padding = " ".repeat(
self.column_length.get() - calculate_print_width(start_location.column).get()
),
code_and_body = RuleCodeAndBody {
message_kind: &message.kind,
show_fix_status: self.show_fix_status
},
)?;
{
if self.show_source {
use std::fmt::Write;
let mut padded = PadAdapter::new(f);
write!(padded, "{}", MessageCodeFrame { message })?;
@@ -125,16 +142,6 @@ impl Display for DisplayGroupedMessage<'_> {
}
}
fn num_digits(n: usize) -> usize {
std::iter::successors(Some(n), |n| {
let next = n / 10;
(next > 0).then_some(next)
})
.count()
.max(1)
}
/// Adapter that adds a ' ' at the start of every line without the need to copy the string.
/// Inspired by Rust's `debug_struct()` internal implementation that also uses a `PadAdapter`.
struct PadAdapter<'buf> {
@@ -174,7 +181,7 @@ mod tests {
#[test]
fn default() {
let mut emitter = GroupedEmitter::default();
let mut emitter = GroupedEmitter::default().with_show_source(true);
let content = capture_emitter_output(&mut emitter, &create_messages());
assert_snapshot!(content);
@@ -182,7 +189,9 @@ mod tests {
#[test]
fn fix_status() {
let mut emitter = GroupedEmitter::default().with_show_fix_status(true);
let mut emitter = GroupedEmitter::default()
.with_show_fix_status(true)
.with_show_source(true);
let content = capture_emitter_output(&mut emitter, &create_messages());
assert_snapshot!(content);

View File

@@ -1,9 +1,10 @@
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_diagnostics::Edit;
use ruff_python_ast::source_code::{SourceCode, SourceLocation};
use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use serde_json::json;
use serde_json::{json, Value};
use std::io::Write;
#[derive(Default)]
@@ -34,23 +35,29 @@ impl Serialize for ExpandedMessages<'_> {
let mut s = serializer.serialize_seq(Some(self.messages.len()))?;
for message in self.messages {
let source_code = message.file.to_source_code();
let fix = if message.fix.is_empty() {
None
} else {
Some(json!({
"message": message.kind.suggestion.as_deref(),
"edits": &ExpandedEdits { edits: message.fix.edits() },
"edits": &ExpandedEdits { edits: message.fix.edits(), source_code: &source_code },
}))
};
let start_location = source_code.source_location(message.start());
let end_location = source_code.source_location(message.end());
let noqa_location = source_code.source_location(message.noqa_offset);
let value = json!({
"code": message.kind.rule().noqa_code().to_string(),
"message": message.kind.body,
"fix": fix,
"location": message.location,
"end_location": message.end_location,
"location": start_location,
"end_location": end_location,
"filename": message.filename(),
"noqa_row": message.noqa_row
"noqa_row": noqa_location.row
});
s.serialize_element(&value)?;
@@ -62,6 +69,7 @@ impl Serialize for ExpandedMessages<'_> {
struct ExpandedEdits<'a> {
edits: &'a [Edit],
source_code: &'a SourceCode<'a, 'a>,
}
impl Serialize for ExpandedEdits<'_> {
@@ -72,10 +80,12 @@ impl Serialize for ExpandedEdits<'_> {
let mut s = serializer.serialize_seq(Some(self.edits.len()))?;
for edit in self.edits {
let start_location = self.source_code.source_location(edit.start());
let end_location = self.source_code.source_location(edit.end());
let value = json!({
"content": edit.content().unwrap_or_default(),
"location": edit.location(),
"end_location": edit.end_location()
"location": to_zero_indexed_column(&start_location),
"end_location": to_zero_indexed_column(&end_location)
});
s.serialize_element(&value)?;
@@ -85,6 +95,13 @@ impl Serialize for ExpandedEdits<'_> {
}
}
fn to_zero_indexed_column(location: &SourceLocation) -> Value {
json!({
"row": location.row,
"column": location.column.to_zero_indexed()
})
}
#[cfg(test)]
mod tests {
use crate::message::tests::{capture_emitter_output, create_messages};

View File

@@ -1,6 +1,9 @@
use crate::message::{group_messages_by_filename, Emitter, EmitterContext, Message};
use crate::message::{
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
};
use crate::registry::AsRule;
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
use ruff_python_ast::source_code::{OneIndexed, SourceLocation};
use std::io::Write;
use std::path::Path;
@@ -23,17 +26,29 @@ impl Emitter for JunitEmitter {
.insert("package".to_string(), "org.ruff".to_string());
for message in messages {
let MessageWithLocation {
message,
start_location,
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.kind.body.clone());
let (row, col) = if context.is_jupyter_notebook(message.filename()) {
let location = if context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
(1, 0)
SourceLocation {
row: OneIndexed::from_zero_indexed(0),
column: OneIndexed::from_zero_indexed(0),
}
} else {
(message.location.row(), message.location.column())
start_location
};
status.set_description(format!("line {row}, col {col}, {}", message.kind.body));
status.set_description(format!(
"line {row}, col {col}, {body}",
row = location.row,
col = location.column,
body = message.kind.body
));
let mut case = TestCase::new(
format!("org.ruff.{}", message.kind.rule().noqa_code()),
status,
@@ -43,9 +58,9 @@ impl Emitter for JunitEmitter {
let classname = file_path.parent().unwrap().join(file_stem);
case.set_classname(classname.to_str().unwrap());
case.extra
.insert("line".to_string(), message.location.row().to_string());
.insert("line".to_string(), location.row.to_string());
case.extra
.insert("column".to_string(), message.location.column().to_string());
.insert("column".to_string(), location.column.to_string());
test_suite.add_test_case(case);
}

View File

@@ -8,10 +8,12 @@ mod junit;
mod pylint;
mod text;
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::io::Write;
use std::ops::Deref;
pub use azure::AzureEmitter;
pub use github::GithubEmitter;
@@ -20,49 +22,64 @@ pub use grouped::GroupedEmitter;
pub use json::JsonEmitter;
pub use junit::JunitEmitter;
pub use pylint::PylintEmitter;
pub use rustpython_parser::ast::Location;
pub use text::TextEmitter;
use crate::jupyter::JupyterIndex;
use crate::registry::AsRule;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_python_ast::source_code::SourceFile;
use ruff_python_ast::source_code::{SourceFile, SourceLocation};
#[derive(Debug, PartialEq, Eq)]
pub struct Message {
pub kind: DiagnosticKind,
pub location: Location,
pub end_location: Location,
pub range: TextRange,
pub fix: Fix,
pub file: SourceFile,
pub noqa_row: usize,
pub noqa_offset: TextSize,
}
impl Message {
pub fn from_diagnostic(diagnostic: Diagnostic, file: SourceFile, noqa_row: usize) -> Self {
pub fn from_diagnostic(
diagnostic: Diagnostic,
file: SourceFile,
noqa_offset: TextSize,
) -> Self {
Self {
range: diagnostic.range(),
kind: diagnostic.kind,
location: Location::new(diagnostic.location.row(), diagnostic.location.column() + 1),
end_location: Location::new(
diagnostic.end_location.row(),
diagnostic.end_location.column() + 1,
),
fix: diagnostic.fix,
file,
noqa_row,
noqa_offset,
}
}
pub fn filename(&self) -> &str {
self.file.name()
}
pub fn compute_start_location(&self) -> SourceLocation {
self.file.to_source_code().source_location(self.start())
}
pub fn compute_end_location(&self) -> SourceLocation {
self.file.to_source_code().source_location(self.end())
}
pub const fn start(&self) -> TextSize {
self.range.start()
}
pub const fn end(&self) -> TextSize {
self.range.end()
}
}
impl Ord for Message {
fn cmp(&self, other: &Self) -> Ordering {
(self.filename(), self.location.row(), self.location.column()).cmp(&(
(self.filename(), self.start(), self.kind.rule()).cmp(&(
other.filename(),
other.location.row(),
other.location.column(),
other.start(),
other.kind.rule(),
))
}
}
@@ -73,13 +90,28 @@ impl PartialOrd for Message {
}
}
fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<&str, Vec<&Message>> {
struct MessageWithLocation<'a> {
message: &'a Message,
start_location: SourceLocation,
}
impl Deref for MessageWithLocation<'_> {
type Target = Message;
fn deref(&self) -> &Self::Target {
self.message
}
}
fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<&str, Vec<MessageWithLocation>> {
let mut grouped_messages = BTreeMap::default();
for message in messages {
grouped_messages
.entry(message.filename())
.or_insert_with(Vec::new)
.push(message);
.push(MessageWithLocation {
message,
start_location: message.compute_start_location(),
});
}
grouped_messages
}
@@ -120,11 +152,11 @@ impl<'a> EmitterContext<'a> {
#[cfg(test)]
mod tests {
use crate::message::{Emitter, EmitterContext, Location, Message};
use crate::message::{Emitter, EmitterContext, Message};
use crate::rules::pyflakes::rules::{UndefinedName, UnusedImport, UnusedVariable};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_ast::source_code::SourceFileBuilder;
use ruff_python_ast::types::Range;
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
pub(super) fn create_messages() -> Vec<Message> {
@@ -148,20 +180,20 @@ def fibonacci(n):
context: None,
multiple: false,
},
Range::new(Location::new(1, 7), Location::new(1, 9)),
TextRange::new(TextSize::from(7), TextSize::from(9)),
);
let fib_source = SourceFileBuilder::new("fib.py").source_text(fib).finish();
let fib_source = SourceFileBuilder::new("fib.py", fib).finish();
let unused_variable = Diagnostic::new(
UnusedVariable {
name: "x".to_string(),
},
Range::new(Location::new(6, 4), Location::new(6, 5)),
TextRange::new(TextSize::from(94), TextSize::from(95)),
)
.with_fix(Fix::new(vec![Edit::deletion(
Location::new(6, 4),
Location::new(6, 9),
TextSize::from(94),
TextSize::from(99),
)]));
let file_2 = r#"if a == 1: pass"#;
@@ -170,17 +202,18 @@ def fibonacci(n):
UndefinedName {
name: "a".to_string(),
},
Range::new(Location::new(1, 3), Location::new(1, 4)),
TextRange::new(TextSize::from(3), TextSize::from(4)),
);
let file_2_source = SourceFileBuilder::new("undef.py")
.source_text(file_2)
.finish();
let file_2_source = SourceFileBuilder::new("undef.py", file_2).finish();
let unused_import_start = unused_import.start();
let unused_variable_start = unused_variable.start();
let undefined_name_start = undefined_name.start();
vec![
Message::from_diagnostic(unused_import, fib_source.clone(), 1),
Message::from_diagnostic(unused_variable, fib_source, 1),
Message::from_diagnostic(undefined_name, file_2_source, 1),
Message::from_diagnostic(unused_import, fib_source.clone(), unused_import_start),
Message::from_diagnostic(unused_variable, fib_source, unused_variable_start),
Message::from_diagnostic(undefined_name, file_2_source, undefined_name_start),
]
}

View File

@@ -1,6 +1,7 @@
use crate::fs::relativize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use ruff_python_ast::source_code::OneIndexed;
use std::io::Write;
/// Generate violations in Pylint format.
@@ -19,9 +20,9 @@ impl Emitter for PylintEmitter {
let row = if context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
1
OneIndexed::from_zero_indexed(0)
} else {
message.location.row()
message.compute_start_location().row
};
writeln!(

View File

@@ -46,7 +46,7 @@ expression: content
"column": 6
},
"filename": "fib.py",
"noqa_row": 1
"noqa_row": 6
},
{
"code": "F821",

View File

@@ -4,30 +4,46 @@ use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use annotate_snippets::display_list::{DisplayList, FormatOptions};
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
use bitflags::bitflags;
use colored::Colorize;
use ruff_diagnostics::DiagnosticKind;
use ruff_python_ast::source_code::OneIndexed;
use ruff_text_size::TextRange;
use std::cmp;
use ruff_python_ast::source_code::{OneIndexed, SourceLocation};
use ruff_text_size::{TextRange, TextSize};
use std::borrow::Cow;
use std::fmt::{Display, Formatter};
use std::io::Write;
bitflags! {
#[derive(Default)]
struct EmitterFlags: u8 {
const SHOW_FIX_STATUS = 0b0000_0001;
const SHOW_FIX = 0b0000_0010;
const SHOW_SOURCE = 0b0000_0100;
}
}
#[derive(Default)]
pub struct TextEmitter {
show_fix_status: bool,
show_fix: bool,
flags: EmitterFlags,
}
impl TextEmitter {
#[must_use]
pub fn with_show_fix_status(mut self, show_fix_status: bool) -> Self {
self.show_fix_status = show_fix_status;
self.flags
.set(EmitterFlags::SHOW_FIX_STATUS, show_fix_status);
self
}
#[must_use]
pub fn with_show_fix(mut self, show_fix: bool) -> Self {
self.show_fix = show_fix;
self.flags.set(EmitterFlags::SHOW_FIX, show_fix);
self
}
#[must_use]
pub fn with_show_source(mut self, show_source: bool) -> Self {
self.flags.set(EmitterFlags::SHOW_SOURCE, show_source);
self
}
}
@@ -47,41 +63,48 @@ impl Emitter for TextEmitter {
sep = ":".cyan(),
)?;
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
let (row, col) = if let Some(jupyter_index) = context.jupyter_index(message.filename())
{
write!(
writer,
"cell {cell}{sep}",
cell = jupyter_index.row_to_cell[message.location.row()],
sep = ":".cyan(),
)?;
let start_location = message.compute_start_location();
(
jupyter_index.row_to_row_in_cell[message.location.row()] as usize,
message.location.column(),
)
} else {
(message.location.row(), message.location.column())
};
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
let diagnostic_location =
if let Some(jupyter_index) = context.jupyter_index(message.filename()) {
write!(
writer,
"cell {cell}{sep}",
cell = jupyter_index.row_to_cell[start_location.row.get()],
sep = ":".cyan(),
)?;
SourceLocation {
row: OneIndexed::new(
jupyter_index.row_to_row_in_cell[start_location.row.get()] as usize,
)
.unwrap(),
column: start_location.column,
}
} else {
start_location
};
writeln!(
writer,
"{row}{sep}{col}{sep} {code_and_body}",
row = diagnostic_location.row,
col = diagnostic_location.column,
sep = ":".cyan(),
code_and_body = RuleCodeAndBody {
message_kind: &message.kind,
show_fix_status: self.show_fix_status
show_fix_status: self.flags.contains(EmitterFlags::SHOW_FIX_STATUS)
}
)?;
if message.file.source_code().is_some() {
if self.flags.contains(EmitterFlags::SHOW_SOURCE) {
writeln!(writer, "{}", MessageCodeFrame { message })?;
}
if self.show_fix {
if let Some(diff) = Diff::from_message(message) {
writeln!(writer, "{diff}")?;
}
if self.flags.contains(EmitterFlags::SHOW_FIX) {
if let Some(diff) = Diff::from_message(message) {
writeln!(writer, "{diff}")?;
}
}
}
@@ -135,108 +158,146 @@ pub(super) struct MessageCodeFrame<'a> {
impl Display for MessageCodeFrame<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let Message {
kind,
file,
location,
end_location,
..
kind, file, range, ..
} = self.message;
if let Some(source_code) = file.source_code() {
let suggestion = kind.suggestion.as_deref();
let footer = if suggestion.is_some() {
vec![Annotation {
id: None,
label: suggestion,
annotation_type: AnnotationType::Help,
}]
} else {
Vec::new()
};
let suggestion = kind.suggestion.as_deref();
let footer = if suggestion.is_some() {
vec![Annotation {
id: None,
label: suggestion,
annotation_type: AnnotationType::Help,
}]
} else {
Vec::new()
};
let mut start_index =
OneIndexed::new(cmp::max(1, location.row().saturating_sub(2))).unwrap();
let content_start_index = OneIndexed::new(location.row()).unwrap();
let source_code = file.to_source_code();
// Trim leading empty lines.
while start_index < content_start_index {
if !source_code.line_text(start_index).trim().is_empty() {
break;
}
start_index = start_index.saturating_add(1);
let content_start_index = source_code.line_index(range.start());
let mut start_index = content_start_index.saturating_sub(2);
// Trim leading empty lines.
while start_index < content_start_index {
if !source_code.line_text(start_index).trim().is_empty() {
break;
}
start_index = start_index.saturating_add(1);
}
let content_end_index = source_code.line_index(range.end());
let mut end_index = content_end_index
.saturating_add(2)
.min(OneIndexed::from_zero_indexed(source_code.line_count()));
// Trim trailing empty lines
while end_index > content_end_index {
if !source_code.line_text(end_index).trim().is_empty() {
break;
}
let mut end_index = OneIndexed::new(cmp::min(
end_location.row().saturating_add(2),
source_code.line_count() + 1,
))
.unwrap();
end_index = end_index.saturating_sub(1);
}
let content_end_index = OneIndexed::new(end_location.row()).unwrap();
let start_offset = source_code.line_start(start_index);
let end_offset = source_code.line_end(end_index);
// Trim trailing empty lines
while end_index > content_end_index {
if !source_code.line_text(end_index).trim().is_empty() {
break;
}
let source = replace_whitespace(
source_code.slice(TextRange::new(start_offset, end_offset)),
range - start_offset,
);
end_index = end_index.saturating_sub(1);
}
let start_offset = source_code.line_start(start_index);
let end_offset = source_code.line_end(end_index);
let source_text = &source_code.text()[TextRange::new(start_offset, end_offset)];
let annotation_start_offset =
// Message columns are one indexed
source_code.offset(location.with_col_offset(-1)) - start_offset;
let annotation_end_offset =
source_code.offset(end_location.with_col_offset(-1)) - start_offset;
let start_char = source_text[TextRange::up_to(annotation_start_offset)]
.chars()
.count();
let char_length = source_text
[TextRange::new(annotation_start_offset, annotation_end_offset)]
let start_char = source.text[TextRange::up_to(source.annotation_range.start())]
.chars()
.count();
let label = kind.rule().noqa_code().to_string();
let char_length = source.text[source.annotation_range].chars().count();
let snippet = Snippet {
title: None,
slices: vec![Slice {
source: source_text,
line_start: location.row(),
annotations: vec![SourceAnnotation {
label: &label,
annotation_type: AnnotationType::Error,
range: (start_char, start_char + char_length),
}],
// The origin (file name, line number, and column number) is already encoded
// in the `label`.
origin: None,
fold: false,
let label = kind.rule().noqa_code().to_string();
let snippet = Snippet {
title: None,
slices: vec![Slice {
source: &source.text,
line_start: content_start_index.get(),
annotations: vec![SourceAnnotation {
label: &label,
annotation_type: AnnotationType::Error,
range: (start_char, start_char + char_length),
}],
footer,
opt: FormatOptions {
#[cfg(test)]
color: false,
#[cfg(not(test))]
color: colored::control::SHOULD_COLORIZE.should_colorize(),
..FormatOptions::default()
},
};
// The origin (file name, line number, and column number) is already encoded
// in the `label`.
origin: None,
fold: false,
}],
footer,
opt: FormatOptions {
#[cfg(test)]
color: false,
#[cfg(not(test))]
color: colored::control::SHOULD_COLORIZE.should_colorize(),
..FormatOptions::default()
},
};
writeln!(f, "{message}", message = DisplayList::from(snippet))?;
}
Ok(())
writeln!(f, "{message}", message = DisplayList::from(snippet))
}
}
fn replace_whitespace(source: &str, annotation_range: TextRange) -> SourceCode {
static TAB_SIZE: TextSize = TextSize::new(4);
let mut result = String::new();
let mut last_end = 0;
let mut range = annotation_range;
let mut column = 0;
for (index, m) in source.match_indices(['\t', '\n', '\r']) {
match m {
"\t" => {
let tab_width = TAB_SIZE - TextSize::new(column % 4);
if index < usize::from(annotation_range.start()) {
range += tab_width - TextSize::new(1);
} else if index < usize::from(annotation_range.end()) {
range = range.add_end(tab_width - TextSize::new(1));
}
result.push_str(&source[last_end..index]);
for _ in 0..u32::from(tab_width) {
result.push(' ');
}
last_end = index + 1;
}
"\n" | "\r" => {
column = 0;
}
_ => unreachable!(),
}
}
// No tabs
if result.is_empty() {
SourceCode {
annotation_range,
text: Cow::Borrowed(source),
}
} else {
result.push_str(&source[last_end..]);
SourceCode {
annotation_range: range,
text: Cow::Owned(result),
}
}
}
struct SourceCode<'a> {
text: Cow<'a, str>,
annotation_range: TextRange,
}
#[cfg(test)]
mod tests {
use crate::message::tests::{capture_emitter_output, create_messages};
@@ -245,7 +306,7 @@ mod tests {
#[test]
fn default() {
let mut emitter = TextEmitter::default();
let mut emitter = TextEmitter::default().with_show_source(true);
let content = capture_emitter_output(&mut emitter, &create_messages());
assert_snapshot!(content);
@@ -253,7 +314,9 @@ mod tests {
#[test]
fn fix_status() {
let mut emitter = TextEmitter::default().with_show_fix_status(true);
let mut emitter = TextEmitter::default()
.with_show_fix_status(true)
.with_show_source(true);
let content = capture_emitter_output(&mut emitter, &create_messages());
assert_snapshot!(content);

View File

@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::fmt::{Display, Write};
use std::fs;
use std::path::Path;
@@ -5,16 +6,13 @@ use std::path::Path;
use anyhow::Result;
use itertools::Itertools;
use log::warn;
use nohash_hasher::IntMap;
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::FxHashMap;
use rustpython_parser::ast::Location;
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::newlines::StrExt;
use ruff_python_ast::source_code::{LineEnding, Locator};
use ruff_python_ast::types::Range;
use ruff_python_ast::newlines::LineEnding;
use ruff_python_ast::source_code::Locator;
use crate::codes::NoqaCode;
use crate::registry::{AsRule, Rule, RuleSet};
@@ -31,46 +29,52 @@ static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").unwrap
#[derive(Debug)]
pub enum Directive<'a> {
None,
All(usize, usize, usize, usize),
Codes(usize, usize, usize, Vec<&'a str>, usize),
// (leading spaces, noqa_range, trailing_spaces)
All(TextSize, TextRange, TextSize),
// (leading spaces, start_offset, end_offset, codes, trailing_spaces)
Codes(TextSize, TextRange, Vec<&'a str>, TextSize),
}
/// Extract the noqa `Directive` from a line of Python source code.
pub fn extract_noqa_directive(line: &str) -> Directive {
match NOQA_LINE_REGEX.captures(line) {
Some(caps) => match caps.name("leading_spaces") {
Some(leading_spaces) => match caps.name("trailing_spaces") {
Some(trailing_spaces) => match caps.name("noqa") {
Some(noqa) => match caps.name("codes") {
Some(codes) => {
let codes: Vec<&str> = SPLIT_COMMA_REGEX
.split(codes.as_str().trim())
.map(str::trim)
.filter(|code| !code.is_empty())
.collect();
if codes.is_empty() {
warn!("Expected rule codes on `noqa` directive: \"{line}\"");
}
Directive::Codes(
leading_spaces.as_str().chars().count(),
noqa.start(),
noqa.end(),
codes,
trailing_spaces.as_str().chars().count(),
)
}
None => Directive::All(
leading_spaces.as_str().chars().count(),
noqa.start(),
noqa.end(),
trailing_spaces.as_str().chars().count(),
),
},
None => Directive::None,
},
None => Directive::None,
},
None => Directive::None,
pub fn extract_noqa_directive<'a>(range: TextRange, locator: &'a Locator) -> Directive<'a> {
let text = &locator.contents()[range];
match NOQA_LINE_REGEX.captures(text) {
Some(caps) => match (
caps.name("leading_spaces"),
caps.name("noqa"),
caps.name("codes"),
caps.name("trailing_spaces"),
) {
(Some(leading_spaces), Some(noqa), Some(codes), Some(trailing_spaces)) => {
let codes: Vec<&str> = SPLIT_COMMA_REGEX
.split(codes.as_str().trim())
.map(str::trim)
.filter(|code| !code.is_empty())
.collect();
let start = range.start() + TextSize::try_from(noqa.start()).unwrap();
if codes.is_empty() {
#[allow(deprecated)]
let line = locator.compute_line_index(start);
warn!("Expected rule codes on `noqa` directive: \"{line}\"");
}
Directive::Codes(
leading_spaces.as_str().text_len(),
TextRange::at(start, noqa.as_str().text_len()),
codes,
trailing_spaces.as_str().text_len(),
)
}
(Some(leading_spaces), Some(noqa), None, Some(trailing_spaces)) => Directive::All(
leading_spaces.as_str().text_len(),
TextRange::at(
range.start() + TextSize::try_from(noqa.start()).unwrap(),
noqa.as_str().text_len(),
),
trailing_spaces.as_str().text_len(),
),
_ => Directive::None,
},
None => Directive::None,
}
@@ -129,16 +133,13 @@ pub fn includes(needle: Rule, haystack: &[&str]) -> bool {
/// Returns `true` if the given [`Rule`] is ignored at the specified `lineno`.
pub fn rule_is_ignored(
code: Rule,
lineno: usize,
noqa_line_for: &IntMap<usize, usize>,
offset: TextSize,
noqa_line_for: &NoqaMapping,
locator: &Locator,
) -> bool {
let noqa_lineno = noqa_line_for.get(&lineno).unwrap_or(&lineno);
let line = locator.slice(Range::new(
Location::new(*noqa_lineno, 0),
Location::new(noqa_lineno + 1, 0),
));
match extract_noqa_directive(line) {
let offset = noqa_line_for.resolve(offset);
let line_range = locator.line_range(offset);
match extract_noqa_directive(line_range, locator) {
Directive::None => false,
Directive::All(..) => true,
Directive::Codes(.., codes, _) => includes(code, &codes),
@@ -153,11 +154,11 @@ pub enum FileExemption {
/// Extract the [`FileExemption`] for a given Python source file, enumerating any rules that are
/// globally ignored within the file.
pub fn file_exemption(lines: &[&str], commented_lines: &[usize]) -> FileExemption {
pub fn file_exemption(contents: &str, comment_ranges: &[TextRange]) -> FileExemption {
let mut exempt_codes: Vec<NoqaCode> = vec![];
for lineno in commented_lines {
match parse_file_exemption(lines[lineno - 1]) {
for range in comment_ranges {
match parse_file_exemption(&contents[*range]) {
ParsedExemption::All => {
return FileExemption::All;
}
@@ -182,17 +183,18 @@ pub fn file_exemption(lines: &[&str], commented_lines: &[usize]) -> FileExemptio
}
}
/// Adds noqa comments to suppress all diagnostics of a file.
pub fn add_noqa(
path: &Path,
diagnostics: &[Diagnostic],
contents: &str,
commented_lines: &[usize],
noqa_line_for: &IntMap<usize, usize>,
locator: &Locator,
commented_lines: &[TextRange],
noqa_line_for: &NoqaMapping,
line_ending: LineEnding,
) -> Result<usize> {
let (count, output) = add_noqa_inner(
diagnostics,
contents,
locator,
commented_lines,
noqa_line_for,
line_ending,
@@ -203,19 +205,19 @@ pub fn add_noqa(
fn add_noqa_inner(
diagnostics: &[Diagnostic],
contents: &str,
commented_lines: &[usize],
noqa_line_for: &IntMap<usize, usize>,
locator: &Locator,
commented_ranges: &[TextRange],
noqa_line_for: &NoqaMapping,
line_ending: LineEnding,
) -> (usize, String) {
// Map of line number to set of (non-ignored) diagnostic codes that are triggered on that line.
let mut matches_by_line: FxHashMap<usize, RuleSet> = FxHashMap::default();
let lines: Vec<&str> = contents.universal_newlines().collect();
// Map of line start offset to set of (non-ignored) diagnostic codes that are triggered on that line.
let mut matches_by_line: BTreeMap<TextSize, (RuleSet, Option<&Directive>)> =
BTreeMap::default();
// Whether the file is exempted from all checks.
// Codes that are globally exempted (within the current file).
let exemption = file_exemption(&lines, commented_lines);
let exemption = file_exemption(locator.contents(), commented_ranges);
let directives = NoqaDirectives::from_commented_ranges(commented_ranges, locator);
// Mark any non-ignored diagnostics.
for diagnostic in diagnostics {
@@ -233,116 +235,122 @@ fn add_noqa_inner(
FileExemption::None => {}
}
let diagnostic_lineno = diagnostic.location.row();
// Is the violation ignored by a `noqa` directive on the parent line?
if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) {
if parent_lineno != diagnostic_lineno {
let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno);
if commented_lines.contains(noqa_lineno) {
match extract_noqa_directive(lines[noqa_lineno - 1]) {
Directive::All(..) => {
if let Some(parent) = diagnostic.parent {
if let Some(directive_line) =
directives.find_line_with_directive(noqa_line_for.resolve(parent))
{
match &directive_line.directive {
Directive::All(..) => {
continue;
}
Directive::Codes(.., codes, _) => {
if includes(diagnostic.kind.rule(), codes) {
continue;
}
Directive::Codes(.., codes, _) => {
if includes(diagnostic.kind.rule(), &codes) {
continue;
}
}
Directive::None => {}
}
Directive::None => {}
}
}
}
// Is the diagnostic ignored by a `noqa` directive on the same line?
let noqa_lineno = noqa_line_for
.get(&diagnostic_lineno)
.unwrap_or(&diagnostic_lineno);
if commented_lines.contains(noqa_lineno) {
match extract_noqa_directive(lines[noqa_lineno - 1]) {
let noqa_offset = noqa_line_for.resolve(diagnostic.start());
// Or ignored by the directive itself
if let Some(directive_line) = directives.find_line_with_directive(noqa_offset) {
match &directive_line.directive {
Directive::All(..) => {
continue;
}
Directive::Codes(.., codes, _) => {
if includes(diagnostic.kind.rule(), &codes) {
continue;
let rule = diagnostic.kind.rule();
if !includes(rule, codes) {
matches_by_line
.entry(directive_line.range.start())
.or_insert_with(|| {
(RuleSet::default(), Some(&directive_line.directive))
})
.0
.insert(rule);
}
continue;
}
Directive::None => {}
}
}
// The diagnostic is not ignored by any `noqa` directive; add it to the list.
let lineno = diagnostic.location.row() - 1;
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
// There's no existing noqa directive that suppresses the diagnostic.
matches_by_line
.entry(noqa_lineno)
.or_default()
.entry(locator.line_start(noqa_offset))
.or_insert_with(|| (RuleSet::default(), None))
.0
.insert(diagnostic.kind.rule());
}
let mut count: usize = 0;
let mut output = String::new();
for (lineno, line) in lines.into_iter().enumerate() {
match matches_by_line.get(&lineno) {
None => {
output.push_str(line);
let mut count = 0;
let mut output = String::with_capacity(locator.len());
let mut prev_end = TextSize::default();
for (offset, (rules, directive)) in matches_by_line {
output.push_str(&locator.contents()[TextRange::new(prev_end, offset)]);
let line = locator.full_line(offset);
match directive {
None | Some(Directive::None) => {
// Add existing content.
output.push_str(line.trim_end());
// Add `noqa` directive.
output.push_str(" # noqa: ");
// Add codes.
push_codes(&mut output, rules.iter().map(|rule| rule.noqa_code()));
output.push_str(&line_ending);
count += 1;
}
Some(Directive::All(..)) => {
// Does not get inserted into the map.
}
Some(Directive::Codes(_, noqa_range, existing, _)) => {
// Reconstruct the line based on the preserved rule codes.
// This enables us to tally the number of edits.
let output_start = output.len();
// Add existing content.
output.push_str(
locator
.slice(TextRange::new(offset, noqa_range.start()))
.trim_end(),
);
// Add `noqa` directive.
output.push_str(" # noqa: ");
// Add codes.
push_codes(
&mut output,
rules
.iter()
.map(|r| r.noqa_code().to_string())
.chain(existing.iter().map(ToString::to_string))
.sorted_unstable(),
);
// Only count if the new line is an actual edit.
if &output[output_start..] != line.trim_end() {
count += 1;
}
output.push_str(&line_ending);
}
Some(rules) => {
match extract_noqa_directive(line) {
Directive::None => {
// Add existing content.
output.push_str(line.trim_end());
// Add `noqa` directive.
output.push_str(" # noqa: ");
// Add codes.
push_codes(&mut output, rules.iter().map(|rule| rule.noqa_code()));
output.push_str(&line_ending);
count += 1;
}
Directive::All(..) => {
// Leave the line as-is.
output.push_str(line);
output.push_str(&line_ending);
}
Directive::Codes(_, start_byte, _, existing, _) => {
// Reconstruct the line based on the preserved rule codes.
// This enables us to tally the number of edits.
let mut formatted = String::with_capacity(line.len());
// Add existing content.
formatted.push_str(line[..start_byte].trim_end());
// Add `noqa` directive.
formatted.push_str(" # noqa: ");
// Add codes.
push_codes(
&mut formatted,
rules
.iter()
.map(|r| r.noqa_code().to_string())
.chain(existing.into_iter().map(ToString::to_string))
.sorted_unstable(),
);
output.push_str(&formatted);
output.push_str(&line_ending);
// Only count if the new line is an actual edit.
if formatted != line {
count += 1;
}
}
};
}
}
prev_end = offset + line.text_len();
}
output.push_str(&locator.contents()[usize::from(prev_end)..]);
(count, output)
}
@@ -352,21 +360,162 @@ fn push_codes<I: Display>(str: &mut String, codes: impl Iterator<Item = I>) {
if !first {
str.push_str(", ");
}
let _ = write!(str, "{code}");
write!(str, "{code}").unwrap();
first = false;
}
}
#[derive(Debug)]
pub(crate) struct NoqaDirectiveLine<'a> {
// The range of the text line for which the noqa directive applies.
pub range: TextRange,
pub directive: Directive<'a>,
pub matches: Vec<NoqaCode>,
}
#[derive(Debug, Default)]
pub(crate) struct NoqaDirectives<'a> {
inner: Vec<NoqaDirectiveLine<'a>>,
}
impl<'a> NoqaDirectives<'a> {
pub fn from_commented_ranges(comment_ranges: &[TextRange], locator: &'a Locator<'a>) -> Self {
let mut directives = Vec::new();
for comment_range in comment_ranges {
let line_range = locator.line_range(comment_range.start());
let directive = match extract_noqa_directive(line_range, locator) {
Directive::None => {
continue;
}
directive @ (Directive::All(..) | Directive::Codes(..)) => directive,
};
// noqa comments are guaranteed to be single line.
directives.push(NoqaDirectiveLine {
range: line_range,
directive,
matches: Vec::new(),
});
}
// Extend a mapping at the end of the file to also include the EOF token.
if let Some(last) = directives.last_mut() {
if last.range.end() == locator.contents().text_len() {
last.range = last.range.add_end(TextSize::from(1));
}
}
Self { inner: directives }
}
pub fn find_line_with_directive(&self, offset: TextSize) -> Option<&NoqaDirectiveLine> {
self.find_line_index(offset).map(|index| &self.inner[index])
}
pub fn find_line_with_directive_mut(
&mut self,
offset: TextSize,
) -> Option<&mut NoqaDirectiveLine<'a>> {
if let Some(index) = self.find_line_index(offset) {
Some(&mut self.inner[index])
} else {
None
}
}
fn find_line_index(&self, offset: TextSize) -> Option<usize> {
self.inner
.binary_search_by(|directive| {
if directive.range.end() < offset {
std::cmp::Ordering::Less
} else if directive.range.contains(offset) {
std::cmp::Ordering::Equal
} else {
std::cmp::Ordering::Greater
}
})
.ok()
}
pub fn lines(&self) -> &[NoqaDirectiveLine] {
&self.inner
}
}
/// Remaps offsets falling into one of the ranges to instead check for a noqa comment on the
/// line specified by the offset.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct NoqaMapping {
ranges: Vec<TextRange>,
}
impl NoqaMapping {
pub(crate) fn with_capacity(capacity: usize) -> Self {
Self {
ranges: Vec::with_capacity(capacity),
}
}
/// Returns the re-mapped position or `position` if no mapping exists.
pub fn resolve(&self, offset: TextSize) -> TextSize {
let index = self.ranges.binary_search_by(|range| {
if range.end() < offset {
std::cmp::Ordering::Less
} else if range.contains(offset) {
std::cmp::Ordering::Equal
} else {
std::cmp::Ordering::Greater
}
});
if let Ok(index) = index {
self.ranges[index].end()
} else {
offset
}
}
pub fn push_mapping(&mut self, range: TextRange) {
if let Some(last_range) = self.ranges.last_mut() {
// Strictly sorted insertion
if last_range.end() <= range.start() {
// OK
}
// Try merging with the last inserted range
else if let Some(intersected) = last_range.intersect(range) {
*last_range = intersected;
return;
} else {
panic!("Ranges must be inserted in sorted order")
}
}
self.ranges.push(range);
}
}
impl FromIterator<TextRange> for NoqaMapping {
fn from_iter<T: IntoIterator<Item = TextRange>>(iter: T) -> Self {
let mut mappings = NoqaMapping::default();
for range in iter {
mappings.push_mapping(range);
}
mappings
}
}
#[cfg(test)]
mod tests {
use nohash_hasher::IntMap;
use rustpython_parser::ast::Location;
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::source_code::LineEnding;
use ruff_python_ast::types::Range;
use ruff_python_ast::newlines::LineEnding;
use ruff_python_ast::source_code::Locator;
use crate::noqa::{add_noqa_inner, NOQA_LINE_REGEX};
use crate::noqa::{add_noqa_inner, NoqaMapping, NOQA_LINE_REGEX};
use crate::rules::pycodestyle::rules::AmbiguousVariableName;
use crate::rules::pyflakes;
@@ -386,87 +535,83 @@ mod tests {
#[test]
fn modification() {
let diagnostics = vec![];
let contents = "x = 1";
let commented_lines = vec![];
let noqa_line_for = IntMap::default();
let noqa_line_for = NoqaMapping::default();
let (count, output) = add_noqa_inner(
&diagnostics,
contents,
&commented_lines,
&[],
&Locator::new(contents),
&[],
&noqa_line_for,
LineEnding::Lf,
);
assert_eq!(count, 0);
assert_eq!(output, format!("{contents}\n"));
assert_eq!(output, format!("{contents}"));
let diagnostics = vec![Diagnostic::new(
let diagnostics = [Diagnostic::new(
pyflakes::rules::UnusedVariable {
name: "x".to_string(),
},
Range::new(Location::new(1, 0), Location::new(1, 0)),
TextRange::new(TextSize::from(0), TextSize::from(0)),
)];
let contents = "x = 1";
let commented_lines = vec![1];
let noqa_line_for = IntMap::default();
let noqa_line_for = NoqaMapping::default();
let (count, output) = add_noqa_inner(
&diagnostics,
contents,
&commented_lines,
&Locator::new(contents),
&[],
&noqa_line_for,
LineEnding::Lf,
);
assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: F841\n");
let diagnostics = vec![
let diagnostics = [
Diagnostic::new(
AmbiguousVariableName("x".to_string()),
Range::new(Location::new(1, 0), Location::new(1, 0)),
TextRange::new(TextSize::from(0), TextSize::from(0)),
),
Diagnostic::new(
pyflakes::rules::UnusedVariable {
name: "x".to_string(),
},
Range::new(Location::new(1, 0), Location::new(1, 0)),
TextRange::new(TextSize::from(0), TextSize::from(0)),
),
];
let contents = "x = 1 # noqa: E741\n";
let commented_lines = vec![1];
let noqa_line_for = IntMap::default();
let noqa_line_for = NoqaMapping::default();
let (count, output) = add_noqa_inner(
&diagnostics,
contents,
&commented_lines,
&Locator::new(contents),
&[TextRange::new(TextSize::from(7), TextSize::from(19))],
&noqa_line_for,
LineEnding::Lf,
);
assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: E741, F841\n");
let diagnostics = vec![
let diagnostics = [
Diagnostic::new(
AmbiguousVariableName("x".to_string()),
Range::new(Location::new(1, 0), Location::new(1, 0)),
TextRange::new(TextSize::from(0), TextSize::from(0)),
),
Diagnostic::new(
pyflakes::rules::UnusedVariable {
name: "x".to_string(),
},
Range::new(Location::new(1, 0), Location::new(1, 0)),
TextRange::new(TextSize::from(0), TextSize::from(0)),
),
];
let contents = "x = 1 # noqa";
let commented_lines = vec![1];
let noqa_line_for = IntMap::default();
let noqa_line_for = NoqaMapping::default();
let (count, output) = add_noqa_inner(
&diagnostics,
contents,
&commented_lines,
&Locator::new(contents),
&[TextRange::new(TextSize::from(7), TextSize::from(13))],
&noqa_line_for,
LineEnding::Lf,
);
assert_eq!(count, 0);
assert_eq!(output, "x = 1 # noqa\n");
assert_eq!(output, "x = 1 # noqa");
}
}

View File

@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
use rustc_hash::FxHashMap;
use crate::resolver::{PyprojectDiscovery, Resolver};
use crate::resolver::{PyprojectConfig, Resolver};
// If we have a Python package layout like:
// - root/
@@ -82,7 +82,7 @@ fn detect_package_root_with_cache<'a>(
pub fn detect_package_roots<'a>(
files: &[&'a Path],
resolver: &'a Resolver,
pyproject_strategy: &'a PyprojectDiscovery,
pyproject_config: &'a PyprojectConfig,
) -> FxHashMap<&'a Path, Option<&'a Path>> {
// Pre-populate the module cache, since the list of files could (but isn't
// required to) contain some `__init__.py` files.
@@ -98,9 +98,7 @@ pub fn detect_package_roots<'a>(
// Search for the package root for each file.
let mut package_roots: FxHashMap<&Path, Option<&Path>> = FxHashMap::default();
for file in files {
let namespace_packages = &resolver
.resolve(file, pyproject_strategy)
.namespace_packages;
let namespace_packages = &resolver.resolve(file, pyproject_config).namespace_packages;
if let Some(package) = file.parent() {
if package_roots.contains_key(package) {
continue;

View File

@@ -156,6 +156,7 @@ ruff_macros::register_rules!(
rules::pylint::rules::BadStringFormatType,
rules::pylint::rules::BidirectionalUnicode,
rules::pylint::rules::BinaryOpException,
rules::pylint::rules::ImportSelf,
rules::pylint::rules::InvalidCharacterBackspace,
rules::pylint::rules::InvalidCharacterSub,
rules::pylint::rules::InvalidCharacterEsc,
@@ -187,6 +188,7 @@ ruff_macros::register_rules!(
rules::pylint::rules::RedefinedLoopName,
rules::pylint::rules::LoggingTooFewArgs,
rules::pylint::rules::LoggingTooManyArgs,
rules::pylint::rules::UnexpectedSpecialMethodSignature,
// flake8-builtins
rules::flake8_builtins::rules::BuiltinVariableShadowing,
rules::flake8_builtins::rules::BuiltinArgumentShadowing,
@@ -491,6 +493,7 @@ ruff_macros::register_rules!(
// flake8-import-conventions
rules::flake8_import_conventions::rules::UnconventionalImportAlias,
rules::flake8_import_conventions::rules::BannedImportAlias,
rules::flake8_import_conventions::rules::BannedImportFrom,
// flake8-datetimez
rules::flake8_datetimez::rules::CallDatetimeWithoutTzinfo,
rules::flake8_datetimez::rules::CallDatetimeToday,
@@ -537,6 +540,9 @@ ruff_macros::register_rules!(
rules::flake8_pyi::rules::UnrecognizedPlatformName,
rules::flake8_pyi::rules::PassInClassBody,
rules::flake8_pyi::rules::DuplicateUnionMember,
rules::flake8_pyi::rules::QuotedAnnotationInStub,
rules::flake8_pyi::rules::SnakeCaseTypeAlias,
rules::flake8_pyi::rules::TSuffixedTypeAlias,
// flake8-pytest-style
rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle,
rules::flake8_pytest_style::rules::PytestFixturePositionalArgs,

View File

@@ -7,10 +7,10 @@ use std::iter::FusedIterator;
///
/// Uses a bitset where a bit of one signals that the Rule with that [u16] is in this set.
#[derive(Clone, Default, CacheKey, PartialEq, Eq)]
pub struct RuleSet([u64; 9]);
pub struct RuleSet([u64; 10]);
impl RuleSet {
const EMPTY: [u64; 9] = [0; 9];
const EMPTY: [u64; 10] = [0; 10];
// 64 fits into a u16 without truncation
#[allow(clippy::cast_possible_truncation)]

View File

@@ -17,25 +17,42 @@ use crate::settings::configuration::Configuration;
use crate::settings::pyproject::settings_toml;
use crate::settings::{pyproject, AllSettings, Settings};
/// The configuration information from a `pyproject.toml` file.
pub struct PyprojectConfig {
/// The strategy used to discover the relevant `pyproject.toml` file for
/// each Python file.
pub strategy: PyprojectDiscoveryStrategy,
/// All settings from the `pyproject.toml` file.
pub settings: AllSettings,
/// Absolute path to the `pyproject.toml` file. This would be `None` when
/// either using the default settings or the `--isolated` flag is set.
pub path: Option<PathBuf>,
}
impl PyprojectConfig {
pub fn new(
strategy: PyprojectDiscoveryStrategy,
settings: AllSettings,
path: Option<PathBuf>,
) -> Self {
Self {
strategy,
settings,
path: path.map(fs::normalize_path),
}
}
}
/// The strategy used to discover the relevant `pyproject.toml` file for each
/// Python file.
#[derive(Debug, is_macro::Is)]
pub enum PyprojectDiscovery {
pub enum PyprojectDiscoveryStrategy {
/// Use a fixed `pyproject.toml` file for all Python files (i.e., one
/// provided on the command-line).
Fixed(AllSettings),
Fixed,
/// Use the closest `pyproject.toml` file in the filesystem hierarchy, or
/// the default settings.
Hierarchical(AllSettings),
}
impl PyprojectDiscovery {
pub fn top_level_settings(&self) -> &AllSettings {
match self {
PyprojectDiscovery::Fixed(settings) => settings,
PyprojectDiscovery::Hierarchical(settings) => settings,
}
}
Hierarchical,
}
/// The strategy for resolving file paths in a `pyproject.toml`.
@@ -75,21 +92,25 @@ impl Resolver {
pub fn resolve_all<'a>(
&'a self,
path: &Path,
strategy: &'a PyprojectDiscovery,
pyproject_config: &'a PyprojectConfig,
) -> &'a AllSettings {
match strategy {
PyprojectDiscovery::Fixed(settings) => settings,
PyprojectDiscovery::Hierarchical(default) => self
match pyproject_config.strategy {
PyprojectDiscoveryStrategy::Fixed => &pyproject_config.settings,
PyprojectDiscoveryStrategy::Hierarchical => self
.settings
.iter()
.rev()
.find_map(|(root, settings)| path.starts_with(root).then_some(settings))
.unwrap_or(default),
.unwrap_or(&pyproject_config.settings),
}
}
pub fn resolve<'a>(&'a self, path: &Path, strategy: &'a PyprojectDiscovery) -> &'a Settings {
&self.resolve_all(path, strategy).lib
pub fn resolve<'a>(
&'a self,
path: &Path,
pyproject_config: &'a PyprojectConfig,
) -> &'a Settings {
&self.resolve_all(path, pyproject_config).lib
}
/// Return an iterator over the resolved [`Settings`] in this [`Resolver`].
@@ -200,7 +221,7 @@ fn match_exclusion<P: AsRef<Path>, R: AsRef<Path>>(
/// Find all Python (`.py`, `.pyi` and `.ipynb` files) in a set of paths.
pub fn python_files_in_path(
paths: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
pyproject_config: &PyprojectConfig,
processor: impl ConfigProcessor,
) -> Result<(Vec<Result<DirEntry, ignore::Error>>, Resolver)> {
// Normalize every path (e.g., convert from relative to absolute).
@@ -209,7 +230,7 @@ pub fn python_files_in_path(
// Search for `pyproject.toml` files in all parent directories.
let mut resolver = Resolver::default();
let mut seen = FxHashSet::default();
if pyproject_strategy.is_hierarchical() {
if pyproject_config.strategy.is_hierarchical() {
for path in &paths {
for ancestor in path.ancestors() {
if seen.insert(ancestor) {
@@ -224,8 +245,8 @@ pub fn python_files_in_path(
}
// Check if the paths themselves are excluded.
if pyproject_strategy.top_level_settings().lib.force_exclude {
paths.retain(|path| !is_file_excluded(path, &resolver, pyproject_strategy));
if pyproject_config.settings.lib.force_exclude {
paths.retain(|path| !is_file_excluded(path, &resolver, pyproject_config));
if paths.is_empty() {
return Ok((vec![], resolver));
}
@@ -240,12 +261,7 @@ pub fn python_files_in_path(
for path in &paths[1..] {
builder.add(path);
}
builder.standard_filters(
pyproject_strategy
.top_level_settings()
.lib
.respect_gitignore,
);
builder.standard_filters(pyproject_config.settings.lib.respect_gitignore);
builder.hidden(false);
let walker = builder.build_parallel();
@@ -261,7 +277,7 @@ pub fn python_files_in_path(
if entry.depth() > 0 {
let path = entry.path();
let resolver = resolver.read().unwrap();
let settings = resolver.resolve(path, pyproject_strategy);
let settings = resolver.resolve(path, pyproject_config);
if let Some(file_name) = path.file_name() {
if !settings.exclude.is_empty()
&& match_exclusion(path, file_name, &settings.exclude)
@@ -283,7 +299,7 @@ pub fn python_files_in_path(
// Search for the `pyproject.toml` file in this directory, before we visit any
// of its contents.
if pyproject_strategy.is_hierarchical() {
if pyproject_config.strategy.is_hierarchical() {
if let Ok(entry) = &result {
if entry
.file_type()
@@ -321,7 +337,7 @@ pub fn python_files_in_path(
// Otherwise, check if the file is included.
let path = entry.path();
let resolver = resolver.read().unwrap();
let settings = resolver.resolve(path, pyproject_strategy);
let settings = resolver.resolve(path, pyproject_config);
if settings.include.is_match(path) {
debug!("Included path via `include`: {:?}", path);
true
@@ -348,10 +364,10 @@ pub fn python_files_in_path(
/// Return `true` if the Python file at [`Path`] is _not_ excluded.
pub fn python_file_at_path(
path: &Path,
pyproject_strategy: &PyprojectDiscovery,
pyproject_config: &PyprojectConfig,
processor: impl ConfigProcessor,
) -> Result<bool> {
if !pyproject_strategy.top_level_settings().lib.force_exclude {
if !pyproject_config.settings.lib.force_exclude {
return Ok(true);
}
@@ -360,7 +376,7 @@ pub fn python_file_at_path(
// Search for `pyproject.toml` files in all parent directories.
let mut resolver = Resolver::default();
if pyproject_strategy.is_hierarchical() {
if pyproject_config.strategy.is_hierarchical() {
for ancestor in path.ancestors() {
if let Some(pyproject) = settings_toml(ancestor)? {
let (root, settings) =
@@ -371,14 +387,14 @@ pub fn python_file_at_path(
}
// Check exclusions.
Ok(!is_file_excluded(&path, &resolver, pyproject_strategy))
Ok(!is_file_excluded(&path, &resolver, pyproject_config))
}
/// Return `true` if the given top-level [`Path`] should be excluded.
fn is_file_excluded(
path: &Path,
resolver: &Resolver,
pyproject_strategy: &PyprojectDiscovery,
pyproject_strategy: &PyprojectConfig,
) -> bool {
// TODO(charlie): Respect gitignore.
for path in path.ancestors() {
@@ -419,7 +435,7 @@ mod tests {
use crate::resolver::{
is_file_excluded, match_exclusion, resolve_settings_with_processor, NoOpProcessor,
PyprojectDiscovery, Relativity, Resolver,
PyprojectConfig, PyprojectDiscoveryStrategy, Relativity, Resolver,
};
use crate::settings::pyproject::find_settings_toml;
use crate::settings::types::FilePattern;
@@ -560,25 +576,29 @@ mod tests {
fn rooted_exclusion() -> Result<()> {
let package_root = test_resource_path("package");
let resolver = Resolver::default();
let ppd = PyprojectDiscovery::Hierarchical(resolve_settings_with_processor(
&find_settings_toml(&package_root)?.unwrap(),
&Relativity::Parent,
&NoOpProcessor,
)?);
let pyproject_config = PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical,
resolve_settings_with_processor(
&find_settings_toml(&package_root)?.unwrap(),
&Relativity::Parent,
&NoOpProcessor,
)?,
None,
);
// src/app.py should not be excluded even if it lives in a hierarchy that should
// be excluded by virtue of the pyproject.toml having `resources/*` in
// it.
assert!(!is_file_excluded(
&package_root.join("src/app.py"),
&resolver,
&ppd,
&pyproject_config,
));
// However, resources/ignored.py should be ignored, since that `resources` is
// beneath the package root.
assert!(is_file_excluded(
&package_root.join("resources/ignored.py"),
&resolver,
&ppd,
&pyproject_config,
));
Ok(())
}

View File

@@ -1,9 +1,8 @@
use rustpython_parser::ast::Location;
use ruff_text_size::TextRange;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::types::Range;
use crate::registry::Rule;
use crate::settings::{flags, Settings};
@@ -49,20 +48,17 @@ fn is_standalone_comment(line: &str) -> bool {
/// ERA001
pub fn commented_out_code(
locator: &Locator,
start: Location,
end: Location,
range: TextRange,
settings: &Settings,
autofix: flags::Autofix,
) -> Option<Diagnostic> {
let location = Location::new(start.row(), 0);
let end_location = Location::new(end.row() + 1, 0);
let line = locator.slice(Range::new(location, end_location));
let line = locator.full_lines(range);
// Verify that the comment is on its own line, and that it contains code.
if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
let mut diagnostic = Diagnostic::new(CommentedOutCode, Range::new(start, end));
let mut diagnostic = Diagnostic::new(CommentedOutCode, range);
if autofix.into() && settings.rules.should_fix(Rule::CommentedOutCode) {
diagnostic.set_fix(Edit::deletion(location, end_location));
diagnostic.set_fix(Edit::range_deletion(locator.full_lines_range(range)));
}
Some(diagnostic)
} else {

View File

@@ -91,4 +91,19 @@ ERA001.py:13:5: ERA001 [*] Found commented-out code
15 14 |
16 15 | #import os # noqa: ERA001
ERA001.py:21:5: ERA001 [*] Found commented-out code
|
21 | class A():
22 | pass
23 | # b = c
| ^^^^^^^ ERA001
|
= help: Remove commented-out code
Suggested fix
18 18 |
19 19 | class A():
20 20 | pass
21 |- # b = c

View File

@@ -3,7 +3,6 @@ use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Located};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
@@ -141,13 +140,13 @@ pub fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
{
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice1, Range::from(value)));
.push(Diagnostic::new(SysVersionSlice1, value.range()));
} else if *i == BigInt::from(3)
&& checker.settings.rules.enabled(Rule::SysVersionSlice3)
{
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice3, Range::from(value)));
.push(Diagnostic::new(SysVersionSlice3, value.range()));
}
}
}
@@ -159,12 +158,12 @@ pub fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
if *i == BigInt::from(2) && checker.settings.rules.enabled(Rule::SysVersion2) {
checker
.diagnostics
.push(Diagnostic::new(SysVersion2, Range::from(value)));
.push(Diagnostic::new(SysVersion2, value.range()));
} else if *i == BigInt::from(0) && checker.settings.rules.enabled(Rule::SysVersion0)
{
checker
.diagnostics
.push(Diagnostic::new(SysVersion0, Range::from(value)));
.push(Diagnostic::new(SysVersion0, value.range()));
}
}
@@ -200,7 +199,7 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
{
checker
.diagnostics
.push(Diagnostic::new(SysVersionInfo0Eq3, Range::from(left)));
.push(Diagnostic::new(SysVersionInfo0Eq3, left.range()));
}
}
} else if *i == BigInt::from(1) {
@@ -219,7 +218,7 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
if checker.settings.rules.enabled(Rule::SysVersionInfo1CmpInt) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionInfo1CmpInt, Range::from(left)));
.push(Diagnostic::new(SysVersionInfo1CmpInt, left.range()));
}
}
}
@@ -246,10 +245,9 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
.rules
.enabled(Rule::SysVersionInfoMinorCmpInt)
{
checker.diagnostics.push(Diagnostic::new(
SysVersionInfoMinorCmpInt,
Range::from(left),
));
checker
.diagnostics
.push(Diagnostic::new(SysVersionInfoMinorCmpInt, left.range()));
}
}
}
@@ -274,12 +272,12 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
if checker.settings.rules.enabled(Rule::SysVersionCmpStr10) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionCmpStr10, Range::from(left)));
.push(Diagnostic::new(SysVersionCmpStr10, left.range()));
}
} else if checker.settings.rules.enabled(Rule::SysVersionCmpStr3) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionCmpStr3, Range::from(left)));
.push(Diagnostic::new(SysVersionCmpStr3, left.range()));
}
}
}
@@ -294,6 +292,6 @@ pub fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
{
checker
.diagnostics
.push(Diagnostic::new(SixPY3, Range::from(expr)));
.push(Diagnostic::new(SixPY3, expr.range()));
}
}

View File

@@ -4,21 +4,19 @@ use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::Edit;
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::types::Range;
/// ANN204
pub fn add_return_annotation(locator: &Locator, stmt: &Stmt, annotation: &str) -> Result<Edit> {
let range = Range::from(stmt);
let contents = locator.slice(range);
let contents = &locator.contents()[stmt.range()];
// Find the colon (following the `def` keyword).
let mut seen_lpar = false;
let mut seen_rpar = false;
let mut count: usize = 0;
for (start, tok, ..) in lexer::lex_located(contents, Mode::Module, range.location).flatten() {
for (tok, range) in lexer::lex_located(contents, Mode::Module, stmt.start()).flatten() {
if seen_lpar && seen_rpar {
if matches!(tok, Tok::Colon) {
return Ok(Edit::insertion(format!(" -> {annotation}"), start));
return Ok(Edit::insertion(format!(" -> {annotation}"), range.start()));
}
}

View File

@@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::types::Range;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{cast, helpers};
use ruff_python_semantic::analyze::visibility;
@@ -446,7 +445,7 @@ fn check_dynamically_typed<F>(
if checker.ctx.match_typing_expr(annotation, "Any") {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
Range::from(annotation),
annotation.range(),
));
};
}
@@ -513,7 +512,7 @@ pub fn definition(
MissingTypeFunctionArgument {
name: arg.node.arg.to_string(),
},
Range::from(arg),
arg.range(),
));
}
}
@@ -544,7 +543,7 @@ pub fn definition(
MissingTypeArgs {
name: arg.node.arg.to_string(),
},
Range::from(arg),
arg.range(),
));
}
}
@@ -575,7 +574,7 @@ pub fn definition(
MissingTypeKwargs {
name: arg.node.arg.to_string(),
},
Range::from(arg),
arg.range(),
));
}
}
@@ -592,7 +591,7 @@ pub fn definition(
MissingTypeCls {
name: arg.node.arg.to_string(),
},
Range::from(arg),
arg.range(),
));
}
} else {
@@ -601,7 +600,7 @@ pub fn definition(
MissingTypeSelf {
name: arg.node.arg.to_string(),
},
Range::from(arg),
arg.range(),
));
}
}

View File

@@ -1,8 +1,8 @@
use ruff_text_size::{TextLen, TextRange};
use rustpython_parser::ast::Stmt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
/// ## What it does
/// Checks for uses of the `assert` keyword.
@@ -37,8 +37,5 @@ impl Violation for Assert {
/// S101
pub fn assert_used(stmt: &Stmt) -> Diagnostic {
Diagnostic::new(
Assert,
Range::new(stmt.location, stmt.location.with_col_offset("assert".len())),
)
Diagnostic::new(Assert, TextRange::at(stmt.start(), "assert".text_len()))
}

View File

@@ -7,7 +7,6 @@ use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::compose_call_path;
use ruff_python_ast::helpers::SimpleCallArgs;
use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker;
@@ -114,7 +113,7 @@ pub fn bad_file_permissions(
if (int_value & WRITE_WORLD > 0) || (int_value & EXECUTE_GROUP > 0) {
checker.diagnostics.push(Diagnostic::new(
BadFilePermissions { mask: int_value },
Range::from(mode_arg),
mode_arg.range(),
));
}
}

View File

@@ -2,7 +2,6 @@ use rustpython_parser::ast::{Expr, ExprKind};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
#[violation]
pub struct ExecBuiltin;
@@ -22,5 +21,5 @@ pub fn exec_used(expr: &Expr, func: &Expr) -> Option<Diagnostic> {
if id != "exec" {
return None;
}
Some(Diagnostic::new(ExecBuiltin, Range::from(expr)))
Some(Diagnostic::new(ExecBuiltin, expr.range()))
}

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
use ruff_text_size::TextRange;
#[violation]
pub struct HardcodedBindAllInterfaces;
@@ -13,9 +13,9 @@ impl Violation for HardcodedBindAllInterfaces {
}
/// S104
pub fn hardcoded_bind_all_interfaces(value: &str, range: &Range) -> Option<Diagnostic> {
pub fn hardcoded_bind_all_interfaces(value: &str, range: TextRange) -> Option<Diagnostic> {
if value == "0.0.0.0" {
Some(Diagnostic::new(HardcodedBindAllInterfaces, *range))
Some(Diagnostic::new(HardcodedBindAllInterfaces, range))
} else {
None
}

View File

@@ -2,7 +2,6 @@ use rustpython_parser::ast::{Arg, Arguments, Expr};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
use super::super::helpers::{matches_password_name, string_literal};
@@ -29,7 +28,7 @@ fn check_password_kwarg(arg: &Arg, default: &Expr) -> Option<Diagnostic> {
HardcodedPasswordDefault {
string: string.to_string(),
},
Range::from(default),
default.range(),
))
}

View File

@@ -2,7 +2,6 @@ use rustpython_parser::ast::Keyword;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
use super::super::helpers::{matches_password_name, string_literal};
@@ -33,7 +32,7 @@ pub fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec<Diagnostic> {
HardcodedPasswordFuncArg {
string: string.to_string(),
},
Range::from(keyword),
keyword.range(),
))
})
.collect()

View File

@@ -2,7 +2,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
use super::super::helpers::{matches_password_name, string_literal};
@@ -52,7 +51,7 @@ pub fn compare_to_hardcoded_password_string(left: &Expr, comparators: &[Expr]) -
HardcodedPasswordString {
string: string.to_string(),
},
Range::from(comp),
comp.range(),
))
})
.collect()
@@ -67,7 +66,7 @@ pub fn assign_hardcoded_password_string(value: &Expr, targets: &[Expr]) -> Optio
HardcodedPasswordString {
string: string.to_string(),
},
Range::from(value),
value.range(),
));
}
}

View File

@@ -5,7 +5,6 @@ use rustpython_parser::ast::{Expr, ExprKind, Operator};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{any_over_expr, unparse_expr};
use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker;
@@ -98,7 +97,7 @@ pub fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
Some(string) if matches_sql_statement(&string) => {
checker
.diagnostics
.push(Diagnostic::new(HardcodedSQLExpression, Range::from(expr)));
.push(Diagnostic::new(HardcodedSQLExpression, expr.range()));
}
_ => (),
}

View File

@@ -2,7 +2,6 @@ use rustpython_parser::ast::Expr;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::Range;
#[violation]
pub struct HardcodedTempFile {
@@ -31,7 +30,7 @@ pub fn hardcoded_tmp_directory(
HardcodedTempFile {
string: value.to_string(),
},
Range::from(expr),
expr.range(),
))
} else {
None

View File

@@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker;
@@ -74,7 +73,7 @@ pub fn hashlib_insecure_hash_functions(
HashlibInsecureHashFunction {
string: hash_func_name.to_string(),
},
Range::from(name_arg),
name_arg.range(),
));
}
}
@@ -91,7 +90,7 @@ pub fn hashlib_insecure_hash_functions(
HashlibInsecureHashFunction {
string: (*func_name).to_string(),
},
Range::from(func),
func.range(),
));
}
}

View File

@@ -3,7 +3,6 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::SimpleCallArgs;
use ruff_python_ast::types::Range;
use crate::checkers::ast::Checker;
@@ -57,20 +56,20 @@ pub fn jinja2_autoescape_false(
if id.as_str() != "select_autoescape" {
checker.diagnostics.push(Diagnostic::new(
Jinja2AutoescapeFalse { value: true },
Range::from(autoescape_arg),
autoescape_arg.range(),
));
}
}
}
_ => checker.diagnostics.push(Diagnostic::new(
Jinja2AutoescapeFalse { value: true },
Range::from(autoescape_arg),
autoescape_arg.range(),
)),
}
} else {
checker.diagnostics.push(Diagnostic::new(
Jinja2AutoescapeFalse { value: false },
Range::from(func),
func.range(),
));
}
}

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