Compare commits

..

2 Commits

Author SHA1 Message Date
Dhruv Manilawala
ce4d4ae6ac Add methods to iter over format spec elements 2024-05-13 14:17:23 +05:30
Dhruv Manilawala
128414cd95 Add Iterator impl for StringLike parts 2024-05-13 13:48:54 +05:30
216 changed files with 1673 additions and 3569 deletions

View File

@@ -167,9 +167,6 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: "Run tests"
shell: bash
env:
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
run: |
cargo nextest run --all-features --profile ci
cargo test --all-features --doc

View File

@@ -1,80 +0,0 @@
name: Sync typeshed
on:
workflow_dispatch:
schedule:
# Run on the 1st and the 15th of every month:
- cron: "0 0 1,15 * *"
env:
FORCE_COLOR: 1
GH_TOKEN: ${{ github.token }}
jobs:
sync:
name: Sync typeshed
runs-on: ubuntu-latest
timeout-minutes: 20
# Don't run the cron job on forks:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
name: Checkout Ruff
with:
path: ruff
- uses: actions/checkout@v4
name: Checkout typeshed
with:
repository: python/typeshed
path: typeshed
- name: Setup git
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- name: Sync typeshed
id: sync
run: |
rm -rf ruff/crates/red_knot/vendor/typeshed
mkdir ruff/crates/red_knot/vendor/typeshed
cp typeshed/README.md ruff/crates/red_knot/vendor/typeshed
cp typeshed/LICENSE ruff/crates/red_knot/vendor/typeshed
cp -r typeshed/stdlib ruff/crates/red_knot/vendor/typeshed/stdlib
rm -rf ruff/crates/red_knot/vendor/typeshed/stdlib/@tests
git -C typeshed rev-parse HEAD > ruff/crates/red_knot/vendor/typeshed/source_commit.txt
- name: Commit the changes
id: commit
if: ${{ steps.sync.outcome == 'success' }}
run: |
cd ruff
git checkout -b typeshedbot/sync-typeshed
git add .
git diff --staged --quiet || git commit -m "Sync typeshed. Source commit: https://github.com/python/typeshed/commit/$(git -C ../typeshed rev-parse HEAD)"
- name: Create a PR
if: ${{ steps.sync.outcome == 'success' && steps.commit.outcome == 'success' }}
run: |
cd ruff
git push --force origin typeshedbot/sync-typeshed
gh pr list --repo $GITHUB_REPOSITORY --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr create --title "Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "internal"
create-issue-on-failure:
name: Create an issue if the typeshed sync failed
runs-on: ubuntu-latest
needs: [sync]
if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && needs.sync.result == 'failure' }}
permissions:
issues: write
steps:
- uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.create({
owner: "astral-sh",
repo: "ruff",
title: `Automated typeshed sync failed on ${new Date().toDateString()}`,
body: "Runs are listed here: https://github.com/astral-sh/ruff/actions/workflows/sync_typeshed.yaml",
})

View File

@@ -637,11 +637,11 @@ Otherwise, follow the instructions from the linux section.
`cargo dev` is a shortcut for `cargo run --package ruff_dev --bin ruff_dev`. You can run some useful
utils with it:
- `cargo dev print-ast <file>`: Print the AST of a python file using Ruff's
[Python parser](https://github.com/astral-sh/ruff/tree/main/crates/ruff_python_parser).
For `if True: pass # comment`, you can see the syntax tree, the byte offsets for start and
stop of each node and also how the `:` token, the comment and whitespace are not represented
anymore:
- `cargo dev print-ast <file>`: Print the AST of a python file using the
[RustPython parser](https://github.com/astral-sh/ruff/tree/main/crates/ruff_python_parser) that is
mainly used in Ruff. For `if True: pass # comment`, you can see the syntax tree, the byte offsets
for start and stop of each node and also how the `:` token, the comment and whitespace are not
represented anymore:
```text
[

View File

@@ -12,28 +12,6 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
license = "MIT"
[workspace.dependencies]
ruff = { path = "crates/ruff" }
ruff_cache = { path = "crates/ruff_cache" }
ruff_diagnostics = { path = "crates/ruff_diagnostics" }
ruff_formatter = { path = "crates/ruff_formatter" }
ruff_index = { path = "crates/ruff_index" }
ruff_linter = { path = "crates/ruff_linter" }
ruff_macros = { path = "crates/ruff_macros" }
ruff_notebook = { path = "crates/ruff_notebook" }
ruff_python_ast = { path = "crates/ruff_python_ast" }
ruff_python_codegen = { path = "crates/ruff_python_codegen" }
ruff_python_formatter = { path = "crates/ruff_python_formatter" }
ruff_python_index = { path = "crates/ruff_python_index" }
ruff_python_literal = { path = "crates/ruff_python_literal" }
ruff_python_parser = { path = "crates/ruff_python_parser" }
ruff_python_semantic = { path = "crates/ruff_python_semantic" }
ruff_python_stdlib = { path = "crates/ruff_python_stdlib" }
ruff_python_trivia = { path = "crates/ruff_python_trivia" }
ruff_server = { path = "crates/ruff_server" }
ruff_source_file = { path = "crates/ruff_source_file" }
ruff_text_size = { path = "crates/ruff_text_size" }
ruff_workspace = { path = "crates/ruff_workspace" }
aho-corasick = { version = "1.1.3" }
annotate-snippets = { version = "0.9.2", features = ["color"] }
anyhow = { version = "1.0.80" }

View File

@@ -266,11 +266,6 @@ The remaining configuration options can be provided through a catch-all `--confi
ruff check --config "lint.per-file-ignores = {'some_file.py' = ['F841']}"
```
To opt in to the latest lint rules, formatter style changes, interface updates, and more, enable
[preview mode](https://docs.astral.sh/ruff/rules/) by setting `preview = true` in your configuration
file or passing `--preview` on the command line. Preview mode enables a collection of unstable
features that may change prior to stabilization.
See `ruff help` for more on Ruff's top-level commands, or `ruff help check` and `ruff help format`
for more on the linting and formatting commands, respectively.

View File

@@ -12,11 +12,11 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ruff_python_parser = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_text_size = { workspace = true }
ruff_index = { workspace = true }
ruff_notebook = { workspace = true }
ruff_python_parser = { path = "../ruff_python_parser" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_text_size = { path = "../ruff_text_size" }
ruff_index = { path = "../ruff_index" }
ruff_notebook = { path = "../ruff_notebook" }
anyhow = { workspace = true }
bitflags = { workspace = true }

View File

@@ -6,4 +6,13 @@ The Red Knot crate contains code working towards multifile analysis, type infere
Red Knot vendors [typeshed](https://github.com/python/typeshed)'s stubs for the standard library. The vendored stubs can be found in `crates/red_knot/vendor/typeshed`. The file `crates/red_knot/vendor/typeshed/source_commit.txt` tells you the typeshed commit that our vendored stdlib stubs currently correspond to.
The typeshed stubs are updated every two weeks via an automated PR using the `sync_typeshed.yaml` workflow in the `.github/workflows` directory. This workflow can also be triggered at any time via [workflow dispatch](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow#running-a-workflow).
Updating the vendored stubs is currently done manually. On a Unix machine, follow the following steps (if you have a typeshed clone in a `typeshed` directory, and a Ruff clone in a `ruff` directory):
```shell
rm -rf ruff/crates/red_knot/vendor/typeshed
mkdir ruff/crates/red_knot/vendor/typeshed
cp typeshed/README.md ruff/crates/red_knot/vendor/typeshed
cp typeshed/LICENSE ruff/crates/red_knot/vendor/typeshed
cp -r typeshed/stdlib ruff/crates/red_knot/vendor/typeshed/stdlib
git -C typeshed rev-parse HEAD > ruff/crates/red_knot/vendor/typeshed/source_commit.txt
```

View File

@@ -1 +1 @@
a9d7e861f7a46ae7acd56569326adef302e10f29
2d33fe212221a05661c0db5215a91cf3d7b7f072

View File

@@ -1,18 +0,0 @@
# Implicit protocols used in importlib.
# We intentionally omit deprecated and optional methods.
from collections.abc import Sequence
from importlib.machinery import ModuleSpec
from types import ModuleType
from typing import Protocol
__all__ = ["LoaderProtocol", "MetaPathFinderProtocol", "PathEntryFinderProtocol"]
class LoaderProtocol(Protocol):
def load_module(self, fullname: str, /) -> ModuleType: ...
class MetaPathFinderProtocol(Protocol):
def find_spec(self, fullname: str, path: Sequence[str] | None, target: ModuleType | None = ..., /) -> ModuleSpec | None: ...
class PathEntryFinderProtocol(Protocol):
def find_spec(self, fullname: str, target: ModuleType | None = ..., /) -> ModuleSpec | None: ...

View File

@@ -31,7 +31,7 @@ from _typeshed import (
)
from collections.abc import Awaitable, Callable, Iterable, Iterator, MutableSet, Reversible, Set as AbstractSet, Sized
from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper
from types import CellType, CodeType, TracebackType
from types import CodeType, TracebackType, _Cell
# mypy crashes if any of {ByteString, Sequence, MutableSequence, Mapping, MutableMapping} are imported from collections.abc in builtins.pyi
from typing import ( # noqa: Y022
@@ -951,7 +951,7 @@ class tuple(Sequence[_T_co]):
class function:
# Make sure this class definition stays roughly in line with `types.FunctionType`
@property
def __closure__(self) -> tuple[CellType, ...] | None: ...
def __closure__(self) -> tuple[_Cell, ...] | None: ...
__code__: CodeType
__defaults__: tuple[Any, ...] | None
__dict__: dict[str, Any]
@@ -1333,7 +1333,7 @@ if sys.version_info >= (3, 11):
locals: Mapping[str, object] | None = None,
/,
*,
closure: tuple[CellType, ...] | None = None,
closure: tuple[_Cell, ...] | None = None,
) -> None: ...
else:
@@ -1794,7 +1794,7 @@ def __import__(
fromlist: Sequence[str] = (),
level: int = 0,
) -> types.ModuleType: ...
def __build_class__(func: Callable[[], CellType | Any], name: str, /, *bases: Any, metaclass: Any = ..., **kwds: Any) -> Any: ...
def __build_class__(func: Callable[[], _Cell | Any], name: str, /, *bases: Any, metaclass: Any = ..., **kwds: Any) -> Any: ...
if sys.version_info >= (3, 10):
from types import EllipsisType

View File

@@ -1,5 +1,3 @@
import sys
from _typeshed import StrOrBytesPath
from collections.abc import Iterator, MutableMapping
from types import TracebackType
from typing import Literal
@@ -93,10 +91,5 @@ class _error(Exception): ...
error: tuple[type[_error], type[OSError]]
if sys.version_info >= (3, 11):
def whichdb(filename: StrOrBytesPath) -> str | None: ...
def open(file: StrOrBytesPath, flag: _TFlags = "r", mode: int = 0o666) -> _Database: ...
else:
def whichdb(filename: str) -> str | None: ...
def open(file: str, flag: _TFlags = "r", mode: int = 0o666) -> _Database: ...
def whichdb(filename: str) -> str | None: ...
def open(file: str, flag: _TFlags = "r", mode: int = 0o666) -> _Database: ...

View File

@@ -1,5 +1,3 @@
import sys
from _typeshed import StrOrBytesPath
from collections.abc import Iterator, MutableMapping
from types import TracebackType
from typing_extensions import Self, TypeAlias
@@ -30,8 +28,4 @@ class _Database(MutableMapping[_KeyType, bytes]):
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
) -> None: ...
if sys.version_info >= (3, 11):
def open(file: StrOrBytesPath, flag: str = "c", mode: int = 0o666) -> _Database: ...
else:
def open(file: str, flag: str = "c", mode: int = 0o666) -> _Database: ...
def open(file: str, flag: str = "c", mode: int = 0o666) -> _Database: ...

View File

@@ -1,5 +1,5 @@
import sys
from _typeshed import ReadOnlyBuffer, StrOrBytesPath
from _typeshed import ReadOnlyBuffer
from types import TracebackType
from typing import TypeVar, overload
from typing_extensions import Self, TypeAlias
@@ -38,7 +38,4 @@ if sys.platform != "win32":
__new__: None # type: ignore[assignment]
__init__: None # type: ignore[assignment]
if sys.version_info >= (3, 11):
def open(filename: StrOrBytesPath, flags: str = "r", mode: int = 0o666, /) -> _gdbm: ...
else:
def open(filename: str, flags: str = "r", mode: int = 0o666, /) -> _gdbm: ...
def open(filename: str, flags: str = "r", mode: int = 0o666, /) -> _gdbm: ...

View File

@@ -1,5 +1,5 @@
import sys
from _typeshed import ReadOnlyBuffer, StrOrBytesPath
from _typeshed import ReadOnlyBuffer
from types import TracebackType
from typing import TypeVar, overload
from typing_extensions import Self, TypeAlias
@@ -34,7 +34,4 @@ if sys.platform != "win32":
__new__: None # type: ignore[assignment]
__init__: None # type: ignore[assignment]
if sys.version_info >= (3, 11):
def open(filename: StrOrBytesPath, flags: str = "r", mode: int = 0o666, /) -> _dbm: ...
else:
def open(filename: str, flags: str = "r", mode: int = 0o666, /) -> _dbm: ...
def open(filename: str, flags: str = "r", mode: int = 0o666, /) -> _dbm: ...

View File

@@ -64,7 +64,7 @@ class SourceLoader(ResourceLoader, ExecutionLoader, metaclass=ABCMeta):
# The base classes differ starting in 3.10:
if sys.version_info >= (3, 10):
# Please keep in sync with _typeshed.importlib.MetaPathFinderProtocol
# Please keep in sync with sys._MetaPathFinder
class MetaPathFinder(metaclass=ABCMeta):
if sys.version_info < (3, 12):
def find_module(self, fullname: str, path: Sequence[str] | None) -> Loader | None: ...
@@ -85,7 +85,7 @@ if sys.version_info >= (3, 10):
def find_spec(self, fullname: str, target: types.ModuleType | None = ...) -> ModuleSpec | None: ...
else:
# Please keep in sync with _typeshed.importlib.MetaPathFinderProtocol
# Please keep in sync with sys._MetaPathFinder
class MetaPathFinder(Finder):
def find_module(self, fullname: str, path: Sequence[str] | None) -> Loader | None: ...
def invalidate_caches(self) -> None: ...

View File

@@ -3,7 +3,6 @@ import importlib.machinery
import sys
import types
from _typeshed import ReadableBuffer, StrOrBytesPath
from _typeshed.importlib import LoaderProtocol
from collections.abc import Callable
from typing import Any
from typing_extensions import ParamSpec
@@ -24,13 +23,13 @@ def source_from_cache(path: str) -> str: ...
def decode_source(source_bytes: ReadableBuffer) -> str: ...
def find_spec(name: str, package: str | None = None) -> importlib.machinery.ModuleSpec | None: ...
def spec_from_loader(
name: str, loader: LoaderProtocol | None, *, origin: str | None = None, is_package: bool | None = None
name: str, loader: importlib.abc.Loader | None, *, origin: str | None = None, is_package: bool | None = None
) -> importlib.machinery.ModuleSpec | None: ...
def spec_from_file_location(
name: str,
location: StrOrBytesPath | None = None,
*,
loader: LoaderProtocol | None = None,
loader: importlib.abc.Loader | None = None,
submodule_search_locations: list[str] | None = ...,
) -> importlib.machinery.ModuleSpec | None: ...
def module_from_spec(spec: importlib.machinery.ModuleSpec) -> types.ModuleType: ...

View File

@@ -1,7 +1,7 @@
import sys
from _typeshed import SupportsRead
from _typeshed.importlib import LoaderProtocol, MetaPathFinderProtocol, PathEntryFinderProtocol
from collections.abc import Callable, Iterable, Iterator
from importlib.abc import Loader, MetaPathFinder, PathEntryFinder
from typing import IO, Any, NamedTuple, TypeVar
from typing_extensions import deprecated
@@ -23,7 +23,7 @@ if sys.version_info < (3, 12):
_PathT = TypeVar("_PathT", bound=Iterable[str])
class ModuleInfo(NamedTuple):
module_finder: MetaPathFinderProtocol | PathEntryFinderProtocol
module_finder: MetaPathFinder | PathEntryFinder
name: str
ispkg: bool
@@ -37,11 +37,11 @@ if sys.version_info < (3, 12):
def __init__(self, fullname: str, file: IO[str], filename: str, etc: tuple[str, str, int]) -> None: ...
@deprecated("Use importlib.util.find_spec() instead. Will be removed in Python 3.14.")
def find_loader(fullname: str) -> LoaderProtocol | None: ...
def get_importer(path_item: str) -> PathEntryFinderProtocol | None: ...
def find_loader(fullname: str) -> Loader | None: ...
def get_importer(path_item: str) -> PathEntryFinder | None: ...
@deprecated("Use importlib.util.find_spec() instead. Will be removed in Python 3.14.")
def get_loader(module_or_name: str) -> LoaderProtocol | None: ...
def iter_importers(fullname: str = "") -> Iterator[MetaPathFinderProtocol | PathEntryFinderProtocol]: ...
def get_loader(module_or_name: str) -> Loader | None: ...
def iter_importers(fullname: str = "") -> Iterator[MetaPathFinder | PathEntryFinder]: ...
def iter_modules(path: Iterable[str] | None = None, prefix: str = "") -> Iterator[ModuleInfo]: ...
def read_code(stream: SupportsRead[bytes]) -> Any: ... # undocumented
def walk_packages(

View File

@@ -1,5 +1,3 @@
import sys
from _typeshed import StrOrBytesPath
from collections.abc import Iterator, MutableMapping
from dbm import _TFlags
from types import TracebackType
@@ -43,17 +41,6 @@ class BsdDbShelf(Shelf[_VT]):
def last(self) -> tuple[str, _VT]: ...
class DbfilenameShelf(Shelf[_VT]):
if sys.version_info >= (3, 11):
def __init__(
self, filename: StrOrBytesPath, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False
) -> None: ...
else:
def __init__(self, filename: str, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False) -> None: ...
def __init__(self, filename: str, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False) -> None: ...
if sys.version_info >= (3, 11):
def open(
filename: StrOrBytesPath, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False
) -> Shelf[Any]: ...
else:
def open(filename: str, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False) -> Shelf[Any]: ...
def open(filename: str, flag: _TFlags = "c", protocol: int | None = None, writeback: bool = False) -> Shelf[Any]: ...

View File

@@ -474,13 +474,6 @@ if sys.version_info >= (3, 12):
ETHERTYPE_VLAN as ETHERTYPE_VLAN,
)
if sys.platform == "linux":
from _socket import ETH_P_ALL as ETH_P_ALL
if sys.platform != "linux" and sys.platform != "win32" and sys.platform != "darwin":
# FreeBSD >= 14.0
from _socket import PF_DIVERT as PF_DIVERT
# Re-exported from errno
EBADF: int
EAGAIN: int
@@ -532,9 +525,6 @@ class AddressFamily(IntEnum):
AF_BLUETOOTH = 32
if sys.platform == "win32" and sys.version_info >= (3, 12):
AF_HYPERV = 34
if sys.platform != "linux" and sys.platform != "win32" and sys.platform != "darwin" and sys.version_info >= (3, 12):
# FreeBSD >= 14.0
AF_DIVERT = 44
AF_INET = AddressFamily.AF_INET
AF_INET6 = AddressFamily.AF_INET6
@@ -587,9 +577,6 @@ if sys.platform != "win32" or sys.version_info >= (3, 9):
if sys.platform == "win32" and sys.version_info >= (3, 12):
AF_HYPERV = AddressFamily.AF_HYPERV
if sys.platform != "linux" and sys.platform != "win32" and sys.platform != "darwin" and sys.version_info >= (3, 12):
# FreeBSD >= 14.0
AF_DIVERT = AddressFamily.AF_DIVERT
class SocketKind(IntEnum):
SOCK_STREAM = 1

View File

@@ -1,8 +1,9 @@
import sys
from _typeshed import OptExcInfo, ProfileFunction, TraceFunction, structseq
from _typeshed.importlib import MetaPathFinderProtocol, PathEntryFinderProtocol
from builtins import object as _object
from collections.abc import AsyncGenerator, Callable, Sequence
from importlib.abc import PathEntryFinder
from importlib.machinery import ModuleSpec
from io import TextIOWrapper
from types import FrameType, ModuleType, TracebackType
from typing import Any, Final, Literal, NoReturn, Protocol, TextIO, TypeVar, final
@@ -14,6 +15,10 @@ _T = TypeVar("_T")
_ExitCode: TypeAlias = str | int | None
_OptExcInfo: TypeAlias = OptExcInfo # noqa: Y047 # TODO: obsolete, remove fall 2022 or later
# Intentionally omits one deprecated and one optional method of `importlib.abc.MetaPathFinder`
class _MetaPathFinder(Protocol):
def find_spec(self, fullname: str, path: Sequence[str] | None, target: ModuleType | None = ..., /) -> ModuleSpec | None: ...
# ----- sys variables -----
if sys.platform != "win32":
abiflags: str
@@ -39,13 +44,13 @@ if sys.version_info >= (3, 12):
last_exc: BaseException # or undefined.
maxsize: int
maxunicode: int
meta_path: list[MetaPathFinderProtocol]
meta_path: list[_MetaPathFinder]
modules: dict[str, ModuleType]
if sys.version_info >= (3, 10):
orig_argv: list[str]
path: list[str]
path_hooks: list[Callable[[str], PathEntryFinderProtocol]]
path_importer_cache: dict[str, PathEntryFinderProtocol | None]
path_hooks: list[Callable[[str], PathEntryFinder]]
path_importer_cache: dict[str, PathEntryFinder | None]
platform: str
if sys.version_info >= (3, 9):
platlibdir: str

View File

@@ -374,11 +374,7 @@ class SpooledTemporaryFile(IO[AnyStr], _SpooledTemporaryFileBase):
def readlines(self, hint: int = ..., /) -> list[AnyStr]: ... # type: ignore[override]
def seek(self, offset: int, whence: int = ...) -> int: ...
def tell(self) -> int: ...
if sys.version_info >= (3, 11):
def truncate(self, size: int | None = None) -> int: ...
else:
def truncate(self, size: int | None = None) -> None: ... # type: ignore[override]
def truncate(self, size: int | None = None) -> None: ... # type: ignore[override]
@overload
def write(self: SpooledTemporaryFile[str], s: str) -> int: ...
@overload

View File

@@ -1,6 +1,5 @@
import sys
from _typeshed import SupportsKeysAndGetItem
from _typeshed.importlib import LoaderProtocol
from collections.abc import (
AsyncGenerator,
Awaitable,
@@ -17,7 +16,7 @@ from collections.abc import (
from importlib.machinery import ModuleSpec
# pytype crashes if types.MappingProxyType inherits from collections.abc.Mapping instead of typing.Mapping
from typing import Any, ClassVar, Literal, Mapping, TypeVar, final, overload # noqa: Y022
from typing import Any, ClassVar, Literal, Mapping, Protocol, TypeVar, final, overload # noqa: Y022
from typing_extensions import ParamSpec, Self, TypeVarTuple, deprecated
__all__ = [
@@ -65,11 +64,18 @@ _T2 = TypeVar("_T2")
_KT = TypeVar("_KT")
_VT_co = TypeVar("_VT_co", covariant=True)
@final
class _Cell:
def __new__(cls, contents: object = ..., /) -> Self: ...
def __eq__(self, value: object, /) -> bool: ...
__hash__: ClassVar[None] # type: ignore[assignment]
cell_contents: Any
# Make sure this class definition stays roughly in line with `builtins.function`
@final
class FunctionType:
@property
def __closure__(self) -> tuple[CellType, ...] | None: ...
def __closure__(self) -> tuple[_Cell, ...] | None: ...
__code__: CodeType
__defaults__: tuple[Any, ...] | None
__dict__: dict[str, Any]
@@ -92,7 +98,7 @@ class FunctionType:
globals: dict[str, Any],
name: str | None = ...,
argdefs: tuple[object, ...] | None = ...,
closure: tuple[CellType, ...] | None = ...,
closure: tuple[_Cell, ...] | None = ...,
) -> Self: ...
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
@overload
@@ -312,12 +318,15 @@ class SimpleNamespace:
def __setattr__(self, name: str, value: Any, /) -> None: ...
def __delattr__(self, name: str, /) -> None: ...
class _LoaderProtocol(Protocol):
def load_module(self, fullname: str, /) -> ModuleType: ...
class ModuleType:
__name__: str
__file__: str | None
@property
def __dict__(self) -> dict[str, Any]: ... # type: ignore[override]
__loader__: LoaderProtocol | None
__loader__: _LoaderProtocol | None
__package__: str | None
__path__: MutableSequence[str]
__spec__: ModuleSpec | None
@@ -327,12 +336,6 @@ class ModuleType:
# using `builtins.__import__` or `importlib.import_module` less painful
def __getattr__(self, name: str) -> Any: ...
@final
class CellType:
def __new__(cls, contents: object = ..., /) -> Self: ...
__hash__: ClassVar[None] # type: ignore[assignment]
cell_contents: Any
_YieldT_co = TypeVar("_YieldT_co", covariant=True)
_SendT_contra = TypeVar("_SendT_contra", contravariant=True)
_ReturnT_co = TypeVar("_ReturnT_co", covariant=True)
@@ -402,7 +405,7 @@ class CoroutineType(Coroutine[_YieldT_co, _SendT_contra, _ReturnT_co]):
@final
class MethodType:
@property
def __closure__(self) -> tuple[CellType, ...] | None: ... # inherited from the added function
def __closure__(self) -> tuple[_Cell, ...] | None: ... # inherited from the added function
@property
def __defaults__(self) -> tuple[Any, ...] | None: ... # inherited from the added function
@property
@@ -567,6 +570,8 @@ def coroutine(func: Callable[_P, Generator[Any, Any, _R]]) -> Callable[_P, Await
@overload
def coroutine(func: _Fn) -> _Fn: ...
CellType = _Cell
if sys.version_info >= (3, 9):
class GenericAlias:
@property

View File

@@ -8,6 +8,7 @@ import typing_extensions
from _collections_abc import dict_items, dict_keys, dict_values
from _typeshed import IdentityFunction, ReadableBuffer, SupportsKeysAndGetItem
from abc import ABCMeta, abstractmethod
from contextlib import AbstractAsyncContextManager, AbstractContextManager
from re import Match as Match, Pattern as Pattern
from types import (
BuiltinFunctionType,
@@ -23,10 +24,10 @@ from types import (
)
from typing_extensions import Never as _Never, ParamSpec as _ParamSpec
if sys.version_info >= (3, 9):
from types import GenericAlias
if sys.version_info >= (3, 10):
from types import UnionType
if sys.version_info >= (3, 9):
from types import GenericAlias
__all__ = [
"AbstractSet",
@@ -401,8 +402,8 @@ class Reversible(Iterable[_T_co], Protocol[_T_co]):
def __reversed__(self) -> Iterator[_T_co]: ...
_YieldT_co = TypeVar("_YieldT_co", covariant=True)
_SendT_contra = TypeVar("_SendT_contra", contravariant=True, default=None)
_ReturnT_co = TypeVar("_ReturnT_co", covariant=True, default=None)
_SendT_contra = TypeVar("_SendT_contra", contravariant=True)
_ReturnT_co = TypeVar("_ReturnT_co", covariant=True)
class Generator(Iterator[_YieldT_co], Generic[_YieldT_co, _SendT_contra, _ReturnT_co]):
def __next__(self) -> _YieldT_co: ...
@@ -427,28 +428,24 @@ class Generator(Iterator[_YieldT_co], Generic[_YieldT_co, _SendT_contra, _Return
@property
def gi_yieldfrom(self) -> Generator[Any, Any, Any] | None: ...
# NOTE: Prior to Python 3.13 these aliases are lacking the second _ExitT_co parameter
if sys.version_info >= (3, 13):
from contextlib import AbstractAsyncContextManager as AsyncContextManager, AbstractContextManager as ContextManager
else:
from contextlib import AbstractAsyncContextManager, AbstractContextManager
# NOTE: Technically we would like this to be able to accept a second parameter as well, just
# like it's counterpart in contextlib, however `typing._SpecialGenericAlias` enforces the
# correct number of arguments at runtime, so we would be hiding runtime errors.
@runtime_checkable
class ContextManager(AbstractContextManager[_T_co, bool | None], Protocol[_T_co]): ...
@runtime_checkable
class ContextManager(AbstractContextManager[_T_co, bool | None], Protocol[_T_co]): ...
@runtime_checkable
class AsyncContextManager(AbstractAsyncContextManager[_T_co, bool | None], Protocol[_T_co]): ...
# NOTE: Technically we would like this to be able to accept a second parameter as well, just
# like it's counterpart in contextlib, however `typing._SpecialGenericAlias` enforces the
# correct number of arguments at runtime, so we would be hiding runtime errors.
@runtime_checkable
class AsyncContextManager(AbstractAsyncContextManager[_T_co, bool | None], Protocol[_T_co]): ...
@runtime_checkable
class Awaitable(Protocol[_T_co]):
@abstractmethod
def __await__(self) -> Generator[Any, Any, _T_co]: ...
# Non-default variations to accommodate couroutines, and `AwaitableGenerator` having a 4th type parameter.
_SendT_contra_nd = TypeVar("_SendT_contra_nd", contravariant=True)
_ReturnT_co_nd = TypeVar("_ReturnT_co_nd", covariant=True)
class Coroutine(Awaitable[_ReturnT_co_nd], Generic[_YieldT_co, _SendT_contra_nd, _ReturnT_co_nd]):
class Coroutine(Awaitable[_ReturnT_co], Generic[_YieldT_co, _SendT_contra, _ReturnT_co]):
__name__: str
__qualname__: str
@property
@@ -460,7 +457,7 @@ class Coroutine(Awaitable[_ReturnT_co_nd], Generic[_YieldT_co, _SendT_contra_nd,
@property
def cr_running(self) -> bool: ...
@abstractmethod
def send(self, value: _SendT_contra_nd, /) -> _YieldT_co: ...
def send(self, value: _SendT_contra, /) -> _YieldT_co: ...
@overload
@abstractmethod
def throw(
@@ -476,9 +473,9 @@ class Coroutine(Awaitable[_ReturnT_co_nd], Generic[_YieldT_co, _SendT_contra_nd,
# The parameters correspond to Generator, but the 4th is the original type.
@type_check_only
class AwaitableGenerator(
Awaitable[_ReturnT_co_nd],
Generator[_YieldT_co, _SendT_contra_nd, _ReturnT_co_nd],
Generic[_YieldT_co, _SendT_contra_nd, _ReturnT_co_nd, _S],
Awaitable[_ReturnT_co],
Generator[_YieldT_co, _SendT_contra, _ReturnT_co],
Generic[_YieldT_co, _SendT_contra, _ReturnT_co, _S],
metaclass=ABCMeta,
): ...

View File

@@ -13,17 +13,17 @@ readme = "../../README.md"
default-run = "ruff"
[dependencies]
ruff_cache = { workspace = true }
ruff_diagnostics = { workspace = true }
ruff_linter = { workspace = true, features = ["clap"] }
ruff_macros = { workspace = true }
ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_formatter = { workspace = true }
ruff_server = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ruff_workspace = { workspace = true }
ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics" }
ruff_linter = { path = "../ruff_linter", features = ["clap"] }
ruff_macros = { path = "../ruff_macros" }
ruff_notebook = { path = "../ruff_notebook" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_server = { path = "../ruff_server" }
ruff_source_file = { path = "../ruff_source_file" }
ruff_text_size = { path = "../ruff_text_size" }
ruff_workspace = { path = "../ruff_workspace" }
anyhow = { workspace = true }
argfile = { workspace = true }
@@ -60,7 +60,7 @@ wild = { workspace = true }
[dev-dependencies]
# Enable test rules during development
ruff_linter = { workspace = true, features = ["clap", "test-rules"] }
ruff_linter = { path = "../ruff_linter", features = ["clap", "test-rules"] }
# Avoid writing colored snapshots when running tests from the terminal
colored = { workspace = true, features = ["no-color"] }
insta = { workspace = true, features = ["filters", "json"] }

View File

@@ -111,13 +111,7 @@ pub enum Command {
output_format: HelpFormat,
},
/// List or describe the available configuration options.
Config {
/// Config key to show
option: Option<String>,
/// Output format
#[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat,
},
Config { option: Option<String> },
/// List all supported upstream linters.
Linter {
/// Output format

View File

@@ -1,38 +1,19 @@
use anyhow::{anyhow, Result};
use crate::args::HelpFormat;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
#[allow(clippy::print_stdout)]
pub(crate) fn config(key: Option<&str>, format: HelpFormat) -> Result<()> {
pub(crate) fn config(key: Option<&str>) -> Result<()> {
match key {
None => {
let metadata = Options::metadata();
match format {
HelpFormat::Text => {
println!("{metadata}");
}
HelpFormat::Json => {
println!("{}", &serde_json::to_string_pretty(&metadata)?);
}
}
}
None => print!("{}", Options::metadata()),
Some(key) => match Options::metadata().find(key) {
None => {
return Err(anyhow!("Unknown option: {key}"));
}
Some(entry) => match format {
HelpFormat::Text => {
print!("{entry}");
}
HelpFormat::Json => {
println!("{}", &serde_json::to_string_pretty(&entry)?);
}
},
Some(entry) => {
print!("{entry}");
}
},
}
Ok(())

View File

@@ -180,11 +180,8 @@ pub fn run(
}
Ok(ExitStatus::Success)
}
Command::Config {
option,
output_format,
} => {
commands::config::config(option.as_deref(), output_format)?;
Command::Config { option } => {
commands::config::config(option.as_deref())?;
Ok(ExitStatus::Success)
}
Command::Linter { output_format } => {

View File

@@ -1,55 +0,0 @@
//! Tests for the `ruff config` subcommand.
use std::process::Command;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
const BIN_NAME: &str = "ruff";
#[test]
fn lint_select() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).arg("config").arg("lint.select"), @r###"
success: true
exit_code: 0
----- stdout -----
A list of rule codes or prefixes to enable. Prefixes can specify exact
rules (like `F841`), entire categories (like `F`), or anything in
between.
When breaking ties between enabled and disabled rules (via `select` and
`ignore`, respectively), more specific prefixes override less
specific prefixes.
Default value: ["E4", "E7", "E9", "F"]
Type: list[RuleSelector]
Example usage:
```toml
# On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).
select = ["E4", "E7", "E9", "F", "B", "Q"]
```
----- stderr -----
"###
);
}
#[test]
fn lint_select_json() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).arg("config").arg("lint.select").arg("--output-format").arg("json"), @r###"
success: true
exit_code: 0
----- stdout -----
{
"doc": "A list of rule codes or prefixes to enable. Prefixes can specify exact\nrules (like `F841`), entire categories (like `F`), or anything in\nbetween.\n\nWhen breaking ties between enabled and disabled rules (via `select` and\n`ignore`, respectively), more specific prefixes override less\nspecific prefixes.",
"default": "[\"E4\", \"E7\", \"E9\", \"F\"]",
"value_type": "list[RuleSelector]",
"scope": null,
"example": "# On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).\nselect = [\"E4\", \"E7\", \"E9\", \"F\", \"B\", \"Q\"]",
"deprecated": null
}
----- stderr -----
"###
);
}

View File

@@ -38,14 +38,14 @@ serde_json = { workspace = true }
url = { workspace = true }
ureq = { workspace = true }
criterion = { workspace = true, default-features = false }
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true }
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true}
[dev-dependencies]
ruff_linter = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_formatter = { workspace = true }
ruff_python_index = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_linter = { path = "../ruff_linter" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_python_index = { path = "../ruff_python_index" }
ruff_python_parser = { path = "../ruff_python_parser" }
[lints]
workspace = true

View File

@@ -10,7 +10,7 @@ use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::source_kind::SourceKind;
use ruff_linter::{registry::Rule, RuleSelector};
use ruff_python_ast::PySourceType;
use ruff_python_parser::{parse_program_tokens, tokenize, Mode};
use ruff_python_parser::{lexer, parse_program_tokens, Mode};
#[cfg(target_os = "windows")]
#[global_allocator]
@@ -55,7 +55,7 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
&case,
|b, case| {
// Tokenize the source.
let tokens = tokenize(case.code(), Mode::Module);
let tokens: Vec<_> = lexer::lex(case.code(), Mode::Module).collect();
// Parse the source.
let ast = parse_program_tokens(tokens.clone(), case.code(), false).unwrap();

View File

@@ -19,7 +19,7 @@ filetime = { workspace = true }
seahash = { workspace = true }
[dev-dependencies]
ruff_macros = { workspace = true }
ruff_macros = { path = "../ruff_macros" }
[lints]
workspace = true

View File

@@ -11,18 +11,18 @@ repository = { workspace = true }
license = { workspace = true }
[dependencies]
ruff = { workspace = true }
ruff_diagnostics = { workspace = true }
ruff_formatter = { workspace = true }
ruff_linter = { workspace = true, features = ["schemars"] }
ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_codegen = { workspace = true }
ruff_python_formatter = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_stdlib = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_workspace = { workspace = true, features = ["schemars"] }
ruff = { path = "../ruff" }
ruff_diagnostics = { path = "../ruff_diagnostics" }
ruff_formatter = { path = "../ruff_formatter" }
ruff_linter = { path = "../ruff_linter", features = ["schemars"] }
ruff_notebook = { path = "../ruff_notebook" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_codegen = { path = "../ruff_python_codegen" }
ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_python_parser = { path = "../ruff_python_parser" }
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
ruff_python_trivia = { path = "../ruff_python_trivia" }
ruff_workspace = { path = "../ruff_workspace", features = ["schemars"] }
anyhow = { workspace = true }
clap = { workspace = true, features = ["wrap_help"] }

View File

@@ -14,7 +14,7 @@ license = { workspace = true }
doctest = false
[dependencies]
ruff_text_size = { workspace = true }
ruff_text_size = { path = "../ruff_text_size" }
anyhow = { workspace = true }
log = { workspace = true }

View File

@@ -11,9 +11,9 @@ repository = { workspace = true }
license = { workspace = true }
[dependencies]
ruff_cache = { workspace = true }
ruff_macros = { workspace = true }
ruff_text_size = { workspace = true }
ruff_cache = { path = "../ruff_cache" }
ruff_macros = { path = "../ruff_macros" }
ruff_text_size = { path = "../ruff_text_size" }
drop_bomb = { workspace = true }
rustc-hash = { workspace = true }

View File

@@ -14,7 +14,7 @@ license = { workspace = true }
doctest = false
[dependencies]
ruff_macros = { workspace = true }
ruff_macros = { path = "../ruff_macros" }
[dev-dependencies]
static_assertions = { workspace = true }

View File

@@ -13,20 +13,20 @@ license = { workspace = true }
[lib]
[dependencies]
ruff_cache = { workspace = true }
ruff_diagnostics = { workspace = true, features = ["serde"] }
ruff_notebook = { workspace = true }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true, features = ["serde"] }
ruff_python_codegen = { workspace = true }
ruff_python_index = { workspace = true }
ruff_python_literal = { workspace = true }
ruff_python_semantic = { workspace = true }
ruff_python_stdlib = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_source_file = { workspace = true, features = ["serde"] }
ruff_text_size = { workspace = true }
ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
ruff_notebook = { path = "../ruff_notebook" }
ruff_macros = { path = "../ruff_macros" }
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
ruff_python_codegen = { path = "../ruff_python_codegen" }
ruff_python_index = { path = "../ruff_python_index" }
ruff_python_literal = { path = "../ruff_python_literal" }
ruff_python_semantic = { path = "../ruff_python_semantic" }
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
ruff_python_trivia = { path = "../ruff_python_trivia" }
ruff_python_parser = { path = "../ruff_python_parser" }
ruff_source_file = { path = "../ruff_source_file", features = ["serde"] }
ruff_text_size = { path = "../ruff_text_size" }
aho-corasick = { workspace = true }
annotate-snippets = { workspace = true, features = ["color"] }

View File

@@ -6,8 +6,8 @@ class Foo2:
"""abc"""
a = 2
"str" # StringLiteral
f"{int}" # FString
"str" # Str (no raise)
f"{int}" # JoinedStr (no raise)
1j # Number (complex)
1 # Number (int)
1.0 # Number (float)
@@ -34,8 +34,8 @@ def foo1():
def foo2():
"""my docstring"""
a = 2
"str" # StringLiteral
f"{int}" # FString
"str" # Str (no raise)
f"{int}" # JoinedStr (no raise)
1j # Number (complex)
1 # Number (int)
1.0 # Number (float)

View File

@@ -1,93 +0,0 @@
# These test cases not only check for `B018` but also verifies that the semantic model
# correctly identifies certain strings as attribute docstring. And, by way of not
# raising the `B018` violation, it can be verified.
a: int
"a: docstring"
b = 1
"b: docstring" " continue"
"b: not a docstring"
c: int = 1
"c: docstring"
_a: int
"_a: docstring"
if True:
d = 1
"d: not a docstring"
(e := 1)
"e: not a docstring"
f = 0
f += 1
"f: not a docstring"
g.h = 1
"g.h: not a docstring"
(i) = 1
"i: docstring"
(j): int = 1
"j: docstring"
(k): int
"k: docstring"
l = m = 1
"l m: not a docstring"
n.a = n.b = n.c = 1
"n.*: not a docstring"
(o, p) = (1, 2)
"o p: not a docstring"
[q, r] = [1, 2]
"q r: not a docstring"
*s = 1
"s: not a docstring"
class Foo:
a = 1
"Foo.a: docstring"
b: int
"Foo.b: docstring"
"Foo.b: not a docstring"
c: int = 1
"Foo.c: docstring"
def __init__(self) -> None:
# This is actually a docstring but we currently don't detect it.
self.x = 1
"self.x: not a docstring"
t = 2
"t: not a docstring"
def random(self):
self.y = 2
"self.y: not a docstring"
u = 2
"u: not a docstring"
def add(self, y: int):
self.x += y
def function():
v = 2
"v: not a docstring"
function.a = 1
"function.a: not a docstring"

View File

@@ -90,10 +90,3 @@ def f():
def func() -> DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]:
...
def f():
from pandas import DataFrame, Series
def func(self) -> DataFrame | list[Series]:
pass

View File

@@ -63,16 +63,3 @@ if (a and
#: Okay
def f():
return 1
# Soft keywords
#: E271
type Number = int
#: E273
type Number = int
#: E275
match(foo):
case(1):
pass

View File

@@ -46,15 +46,3 @@ regex = '\\\_'
#: W605:1:7
u'foo\ bar'
#: W605:1:13
(
"foo \
bar \. baz"
)
#: W605:1:6
"foo \. bar \t"
#: W605:1:13
"foo \t bar \."

View File

@@ -1,4 +1,4 @@
"""__init__.py with nonempty __all__
"""__init__.py with __all__
Unused stdlib and third party imports are unsafe removals
@@ -33,10 +33,10 @@ from . import aliased as aliased # Ok: is redundant alias
from . import exported # Ok: is exported in __all__
from . import unused # F401: add to __all__
# from . import unused # F401: add to __all__
from . import renamed as bees # F401: add to __all__
# from . import renamed as bees # F401: add to __all__
__all__ = ["argparse", "exported"]

View File

@@ -1,11 +0,0 @@
"""__init__.py with empty __all__
"""
from . import unused # F401: add to __all__
from . import renamed as bees # F401: add to __all__
__all__ = []

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1,11 +0,0 @@
"""__init__.py with mis-typed __all__
"""
from . import unused # F401: recommend add to all w/o fix
from . import renamed as bees # F401: recommend add to all w/o fix
__all__ = None

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1,8 +0,0 @@
"""__init__.py with multiple imports added to all in one edit
"""
from . import unused, renamed as bees # F401: add to __all__
__all__ = [];

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1,16 +0,0 @@
"""__init__.py with __all__ populated by conditional plus-eq
multiple __all__ so cannot offer a fix to add to them
"""
import sys
from . import unused, exported, renamed as bees
if sys.version_info > (3, 9):
from . import also_exported
__all__ = ["exported"]
if sys.version_info >= (3, 9):
__all__ += ["also_exported"]

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -38,12 +38,3 @@ z = (
else
y
)
# FURB110
z = (
x
if x
else y
if y > 0
else None
)

View File

@@ -103,7 +103,7 @@ def f():
def f():
# Invalid - nonexistent error code with multibyte character
# Invalid - nonexistant error code with multibyte character
d = 1 # …noqa: F841, E50
e = 1 # …noqa: E50

View File

@@ -1065,17 +1065,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyflakes::rules::invalid_print_syntax(checker, left);
}
}
Expr::BinOp(
bin_op @ ast::ExprBinOp {
left,
op: Operator::Mod,
right,
range: _,
},
) => {
if let Expr::StringLiteral(format_string @ ast::ExprStringLiteral { value, .. }) =
left.as_ref()
{
Expr::BinOp(ast::ExprBinOp {
left,
op: Operator::Mod,
right,
range: _,
}) => {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = left.as_ref() {
if checker.any_enabled(&[
Rule::PercentFormatInvalidFormat,
Rule::PercentFormatExpectedMapping,
@@ -1155,14 +1151,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::printf_string_formatting(checker, expr, right);
}
if checker.enabled(Rule::BadStringFormatCharacter) {
pylint::rules::bad_string_format_character::percent(
checker,
expr,
format_string,
);
pylint::rules::bad_string_format_character::percent(checker, expr);
}
if checker.enabled(Rule::BadStringFormatType) {
pylint::rules::bad_string_format_type(checker, bin_op, format_string);
pylint::rules::bad_string_format_type(checker, expr, right);
}
if checker.enabled(Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(checker, expr);

View File

@@ -2,7 +2,7 @@ use ruff_python_ast::StringLike;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_bandit, flake8_pyi, flake8_quotes, pycodestyle, ruff};
use crate::rules::{flake8_bandit, flake8_pyi, flake8_quotes, ruff};
/// Run lint rules over a [`StringLike`] syntax nodes.
pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
@@ -36,7 +36,4 @@ pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
if checker.enabled(Rule::AvoidableEscapedQuote) && checker.settings.flake8_quotes.avoid_escape {
flake8_quotes::rules::avoidable_escaped_quote(checker, string_like);
}
if checker.enabled(Rule::InvalidEscapeSequence) {
pycodestyle::rules::invalid_escape_sequence(checker, string_like);
}
}

View File

@@ -5,13 +5,13 @@ use std::path::Path;
use ruff_notebook::CellOffsets;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_diagnostics::Diagnostic;
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use crate::directives::TodoComment;
use crate::linter::TokenSource;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle::rules::BlankLinesChecker;
use crate::rules::{
@@ -22,7 +22,7 @@ use crate::settings::LinterSettings;
#[allow(clippy::too_many_arguments)]
pub(crate) fn check_tokens(
tokens: &TokenSource,
tokens: &[LexResult],
path: &Path,
locator: &Locator,
indexer: &Indexer,
@@ -42,7 +42,7 @@ pub(crate) fn check_tokens(
Rule::BlankLinesBeforeNestedDefinition,
]) {
BlankLinesChecker::new(locator, stylist, settings, source_type, cell_offsets)
.check_lines(tokens.kinds(), &mut diagnostics);
.check_lines(tokens, &mut diagnostics);
}
if settings.rules.enabled(Rule::BlanketTypeIgnore) {
@@ -75,6 +75,18 @@ pub(crate) fn check_tokens(
pyupgrade::rules::unnecessary_coding_comment(&mut diagnostics, locator, indexer);
}
if settings.rules.enabled(Rule::InvalidEscapeSequence) {
for (tok, range) in tokens.iter().flatten() {
pycodestyle::rules::invalid_escape_sequence(
&mut diagnostics,
locator,
indexer,
tok,
*range,
);
}
}
if settings.rules.enabled(Rule::TabIndentation) {
pycodestyle::rules::tab_indentation(&mut diagnostics, locator, indexer);
}
@@ -86,8 +98,8 @@ pub(crate) fn check_tokens(
Rule::InvalidCharacterNul,
Rule::InvalidCharacterZeroWidthSpace,
]) {
for (token, range) in tokens.kinds() {
pylint::rules::invalid_string_characters(&mut diagnostics, token, range, locator);
for (tok, range) in tokens.iter().flatten() {
pylint::rules::invalid_string_characters(&mut diagnostics, tok, *range, locator);
}
}
@@ -98,7 +110,7 @@ pub(crate) fn check_tokens(
]) {
pycodestyle::rules::compound_statements(
&mut diagnostics,
tokens.kinds(),
tokens,
locator,
indexer,
source_type,
@@ -112,7 +124,7 @@ pub(crate) fn check_tokens(
]) {
flake8_implicit_str_concat::rules::implicit(
&mut diagnostics,
tokens.kinds(),
tokens,
settings,
locator,
indexer,
@@ -124,11 +136,11 @@ pub(crate) fn check_tokens(
Rule::TrailingCommaOnBareTuple,
Rule::ProhibitedTrailingComma,
]) {
flake8_commas::rules::trailing_commas(&mut diagnostics, tokens.kinds(), locator, indexer);
flake8_commas::rules::trailing_commas(&mut diagnostics, tokens, locator, indexer);
}
if settings.rules.enabled(Rule::ExtraneousParentheses) {
pyupgrade::rules::extraneous_parentheses(&mut diagnostics, tokens.kinds(), locator);
pyupgrade::rules::extraneous_parentheses(&mut diagnostics, tokens, locator);
}
if source_type.is_stub() && settings.rules.enabled(Rule::TypeCommentInStub) {
@@ -172,7 +184,7 @@ pub(crate) fn check_tokens(
}
if settings.rules.enabled(Rule::TooManyNewlinesAtEndOfFile) {
pycodestyle::rules::too_many_newlines_at_end_of_file(&mut diagnostics, tokens.kinds());
pycodestyle::rules::too_many_newlines_at_end_of_file(&mut diagnostics, tokens);
}
diagnostics.retain(|diagnostic| settings.rules.enabled(diagnostic.kind.rule()));

View File

@@ -131,7 +131,7 @@ fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer
// For multi-line strings, we expect `noqa` directives on the last line of the
// string.
Tok::String { flags, .. } if flags.is_triple_quoted() => {
Tok::String { kind, .. } if kind.is_triple_quoted() => {
if locator.contains_line_break(*range) {
string_mappings.push(TextRange::new(
locator.line_start(range.start()),

View File

@@ -4,26 +4,27 @@
use std::iter::FusedIterator;
use ruff_python_ast::{self as ast, Stmt, Suite};
use ruff_python_parser::{TokenKind, TokenKindIter};
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_text_size::{Ranged, TextSize};
use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor};
use ruff_source_file::{Locator, UniversalNewlineIterator};
/// Extract doc lines (standalone comments) from a token sequence.
pub(crate) fn doc_lines_from_tokens(tokens: TokenKindIter) -> DocLines {
DocLines::new(tokens)
pub(crate) fn doc_lines_from_tokens(lxr: &[LexResult]) -> DocLines {
DocLines::new(lxr)
}
pub(crate) struct DocLines<'a> {
inner: TokenKindIter<'a>,
inner: std::iter::Flatten<core::slice::Iter<'a, LexResult>>,
prev: TextSize,
}
impl<'a> DocLines<'a> {
fn new(tokens: TokenKindIter<'a>) -> Self {
fn new(lxr: &'a [LexResult]) -> Self {
Self {
inner: tokens,
inner: lxr.iter().flatten(),
prev: TextSize::default(),
}
}
@@ -38,15 +39,15 @@ impl Iterator for DocLines<'_> {
let (tok, range) = self.inner.next()?;
match tok {
TokenKind::Comment => {
Tok::Comment(..) => {
if at_start_of_line {
break Some(range.start());
}
}
TokenKind::Newline | TokenKind::NonLogicalNewline => {
Tok::Newline | Tok::NonLogicalNewline => {
at_start_of_line = true;
}
TokenKind::Indent | TokenKind::Dedent => {
Tok::Indent | Tok::Dedent => {
// ignore
}
_ => {

View File

@@ -1,12 +1,10 @@
//! Interface for generating fix edits from higher-level actions (e.g., "remove an argument").
use std::borrow::Cow;
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Stmt};
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
@@ -126,7 +124,7 @@ pub(crate) fn remove_unused_imports<'a>(
/// Edits to make the specified imports explicit, e.g. change `import x` to `import x as x`.
pub(crate) fn make_redundant_alias<'a>(
member_names: impl Iterator<Item = Cow<'a, str>>,
member_names: impl Iterator<Item = &'a str>,
stmt: &Stmt,
) -> Vec<Edit> {
let aliases = match stmt {
@@ -146,53 +144,6 @@ pub(crate) fn make_redundant_alias<'a>(
.collect()
}
/// Fix to add the specified imports to the `__all__` export list.
pub(crate) fn add_to_dunder_all<'a>(
names: impl Iterator<Item = &'a str>,
expr: &Expr,
stylist: &Stylist,
) -> Vec<Edit> {
let (insertion_point, export_prefix_length) = match expr {
Expr::List(ExprList { elts, range, .. }) => (
elts.last()
.map_or(range.end() - "]".text_len(), Ranged::end),
elts.len(),
),
Expr::Tuple(tup) if tup.parenthesized => (
tup.elts
.last()
.map_or(tup.end() - ")".text_len(), Ranged::end),
tup.elts.len(),
),
Expr::Tuple(tup) if !tup.parenthesized => (
tup.elts
.last()
.expect("unparenthesized empty tuple is not possible")
.range()
.end(),
tup.elts.len(),
),
_ => {
// we don't know how to insert into this expression
return vec![];
}
};
let quote = stylist.quote();
let mut edits: Vec<_> = names
.enumerate()
.map(|(offset, name)| match export_prefix_length + offset {
0 => Edit::insertion(format!("{quote}{name}{quote}"), insertion_point),
_ => Edit::insertion(format!(", {quote}{name}{quote}"), insertion_point),
})
.collect();
if let Expr::Tuple(tup) = expr {
if tup.parenthesized && export_prefix_length + edits.len() == 1 {
edits.push(Edit::insertion(",".to_string(), insertion_point));
}
}
edits
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum Parentheses {
/// Remove parentheses, if the removed argument is the only argument left.
@@ -526,20 +477,14 @@ fn all_lines_fit(
#[cfg(test)]
mod tests {
use anyhow::{anyhow, Result};
use std::borrow::Cow;
use test_case::test_case;
use anyhow::Result;
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_codegen::Stylist;
use ruff_python_parser::{lexer, parse_expression, parse_suite, Mode};
use ruff_diagnostics::Edit;
use ruff_python_parser::parse_suite;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::fix::apply_fixes;
use crate::fix::edits::{
add_to_dunder_all, make_redundant_alias, next_stmt_break, trailing_semicolon,
};
use crate::fix::edits::{make_redundant_alias, next_stmt_break, trailing_semicolon};
#[test]
fn find_semicolon() -> Result<()> {
@@ -617,7 +562,7 @@ x = 1 \
let program = parse_suite(contents).unwrap();
let stmt = program.first().unwrap();
assert_eq!(
make_redundant_alias(["x"].into_iter().map(Cow::from), stmt),
make_redundant_alias(["x"].into_iter(), stmt),
vec![Edit::range_replacement(
String::from("x as x"),
TextRange::new(TextSize::new(7), TextSize::new(8)),
@@ -625,7 +570,7 @@ x = 1 \
"make just one item redundant"
);
assert_eq!(
make_redundant_alias(vec!["x", "y"].into_iter().map(Cow::from), stmt),
make_redundant_alias(vec!["x", "y"].into_iter(), stmt),
vec![Edit::range_replacement(
String::from("x as x"),
TextRange::new(TextSize::new(7), TextSize::new(8)),
@@ -633,7 +578,7 @@ x = 1 \
"the second item is already a redundant alias"
);
assert_eq!(
make_redundant_alias(vec!["x", "z"].into_iter().map(Cow::from), stmt),
make_redundant_alias(vec!["x", "z"].into_iter(), stmt),
vec![Edit::range_replacement(
String::from("x as x"),
TextRange::new(TextSize::new(7), TextSize::new(8)),
@@ -641,47 +586,4 @@ x = 1 \
"the third item is already aliased to something else"
);
}
#[test_case("()", &["x", "y"], r#"("x", "y")"# ; "2 into empty tuple")]
#[test_case("()", &["x"], r#"("x",)"# ; "1 into empty tuple adding a trailing comma")]
#[test_case("[]", &["x", "y"], r#"["x", "y"]"# ; "2 into empty list")]
#[test_case("[]", &["x"], r#"["x"]"# ; "1 into empty list")]
#[test_case(r#""a", "b""#, &["x", "y"], r#""a", "b", "x", "y""# ; "2 into unparenthesized tuple")]
#[test_case(r#""a", "b""#, &["x"], r#""a", "b", "x""# ; "1 into unparenthesized tuple")]
#[test_case(r#""a", "b","#, &["x", "y"], r#""a", "b", "x", "y","# ; "2 into unparenthesized tuple w/trailing comma")]
#[test_case(r#""a", "b","#, &["x"], r#""a", "b", "x","# ; "1 into unparenthesized tuple w/trailing comma")]
#[test_case(r#"("a", "b")"#, &["x", "y"], r#"("a", "b", "x", "y")"# ; "2 into nonempty tuple")]
#[test_case(r#"("a", "b")"#, &["x"], r#"("a", "b", "x")"# ; "1 into nonempty tuple")]
#[test_case(r#"("a", "b",)"#, &["x", "y"], r#"("a", "b", "x", "y",)"# ; "2 into nonempty tuple w/trailing comma")]
#[test_case(r#"("a", "b",)"#, &["x"], r#"("a", "b", "x",)"# ; "1 into nonempty tuple w/trailing comma")]
#[test_case(r#"["a", "b",]"#, &["x", "y"], r#"["a", "b", "x", "y",]"# ; "2 into nonempty list w/trailing comma")]
#[test_case(r#"["a", "b",]"#, &["x"], r#"["a", "b", "x",]"# ; "1 into nonempty list w/trailing comma")]
#[test_case(r#"["a", "b"]"#, &["x", "y"], r#"["a", "b", "x", "y"]"# ; "2 into nonempty list")]
#[test_case(r#"["a", "b"]"#, &["x"], r#"["a", "b", "x"]"# ; "1 into nonempty list")]
fn add_to_dunder_all_test(raw: &str, names: &[&str], expect: &str) -> Result<()> {
let locator = Locator::new(raw);
let edits = {
let expr = parse_expression(raw)?;
let stylist = Stylist::from_tokens(
&lexer::lex(raw, Mode::Expression).collect::<Vec<_>>(),
&locator,
);
// SUT
add_to_dunder_all(names.iter().copied(), &expr, &stylist)
};
let diag = {
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
let mut iter = edits.into_iter();
Diagnostic::new(
MissingNewlineAtEndOfFile, // The choice of rule here is arbitrary.
TextRange::default(),
)
.with_fix(Fix::safe_edits(
iter.next().ok_or(anyhow!("expected edits nonempty"))?,
iter,
))
};
assert_eq!(apply_fixes([diag].iter(), &locator).code, expect);
Ok(())
}
}

View File

@@ -321,6 +321,7 @@ mod tests {
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{parse_suite, Mode};
use ruff_source_file::{LineEnding, Locator};
use ruff_text_size::TextSize;
@@ -331,7 +332,7 @@ mod tests {
fn start_of_file() -> Result<()> {
fn insert(contents: &str) -> Result<Insertion> {
let program = parse_suite(contents)?;
let tokens = ruff_python_parser::tokenize(contents, Mode::Module);
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, Mode::Module);
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator);
Ok(Insertion::start_of_file(&program, &locator, &stylist))
@@ -442,7 +443,7 @@ x = 1
#[test]
fn start_of_block() {
fn insert(contents: &str, offset: TextSize) -> Insertion {
let tokens = ruff_python_parser::tokenize(contents, Mode::Module);
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, Mode::Module);
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator);
Insertion::start_of_block(offset, &locator, &stylist, PySourceType::default())

View File

@@ -14,7 +14,7 @@ use ruff_python_ast::{PySourceType, Suite};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{AsMode, ParseError, TokenKindIter, Tokens};
use ruff_python_parser::{AsMode, ParseError};
use ruff_source_file::{Locator, SourceFileBuilder};
use ruff_text_size::Ranged;
@@ -93,7 +93,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.kinds()));
doc_lines.extend(doc_lines_from_tokens(&tokens));
}
// Run the token-based rules.
@@ -353,7 +353,7 @@ pub fn add_noqa_to_path(
let contents = source_kind.source_code();
// Tokenize once.
let tokens = ruff_python_parser::tokenize(contents, source_type.as_mode());
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(contents);
@@ -518,7 +518,8 @@ pub fn lint_fix<'a>(
// Continuously fix until the source code stabilizes.
loop {
// Tokenize once.
let tokens = ruff_python_parser::tokenize(transformed.source_code(), source_type.as_mode());
let tokens: Vec<LexResult> =
ruff_python_parser::tokenize(transformed.source_code(), source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(transformed.source_code());
@@ -714,7 +715,7 @@ impl<'a> ParseSource<'a> {
#[derive(Debug, Clone)]
pub enum TokenSource<'a> {
/// Use the precomputed tokens to generate the AST.
Tokens(Tokens),
Tokens(Vec<LexResult>),
/// Use the precomputed tokens and AST.
Precomputed {
tokens: &'a [LexResult],
@@ -722,18 +723,6 @@ pub enum TokenSource<'a> {
},
}
impl TokenSource<'_> {
/// Returns an iterator over the [`TokenKind`] and the corresponding range.
///
/// [`TokenKind`]: ruff_python_parser::TokenKind
pub fn kinds(&self) -> TokenKindIter {
match self {
TokenSource::Tokens(tokens) => tokens.kinds(),
TokenSource::Precomputed { tokens, .. } => TokenKindIter::new(tokens),
}
}
}
impl Deref for TokenSource<'_> {
type Target = [LexResult];

View File

@@ -270,6 +270,7 @@ impl Rule {
| Rule::InvalidCharacterNul
| Rule::InvalidCharacterSub
| Rule::InvalidCharacterZeroWidthSpace
| Rule::InvalidEscapeSequence
| Rule::InvalidTodoCapitalization
| Rule::InvalidTodoTag
| Rule::LineContainsFixme

View File

@@ -57,7 +57,7 @@ pub(crate) fn hardcoded_bind_all_interfaces(checker: &mut Checker, string: Strin
}
}
ast::FStringPart::FString(f_string) => {
for literal in f_string.elements.literals() {
for literal in f_string.literals() {
if &**literal == "0.0.0.0" {
checker.diagnostics.push(Diagnostic::new(
HardcodedBindAllInterfaces,

View File

@@ -64,7 +64,7 @@ pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: StringLike)
check(checker, literal, literal.range());
}
ast::FStringPart::FString(f_string) => {
for literal in f_string.elements.literals() {
for literal in f_string.literals() {
check(checker, literal, literal.range());
}
}

View File

@@ -13,7 +13,6 @@ mod tests {
use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::settings::LinterSettings;
use crate::test::test_path;
@@ -63,7 +62,6 @@ mod tests {
#[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018.ipynb"))]
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018_attribute_docstring.py"))]
#[test_case(Rule::LoopIteratorMutation, Path::new("B909.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
@@ -75,26 +73,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::UselessExpression, Path::new("B018.ipynb"))]
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018_attribute_docstring.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn zip_without_explicit_strict() -> Result<()> {
let snapshot = "B905.py";

View File

@@ -16,9 +16,6 @@ use super::super::helpers::at_last_top_level_expression_in_cell;
/// by mistake. Assign a useless expression to a variable, or remove it
/// entirely.
///
/// In [preview mode], this rule will also flag string literals and f-strings that
/// are not used as a docstring or an attribute docstring.
///
/// ## Example
/// ```python
/// 1 + 1
@@ -48,8 +45,6 @@ use super::super::helpers::at_last_top_level_expression_in_cell;
/// with errors.ExceptionRaisedContext():
/// _ = obj.attribute
/// ```
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[violation]
pub struct UselessExpression {
kind: Kind,
@@ -74,16 +69,15 @@ impl Violation for UselessExpression {
/// B018
pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) {
// Ignore comparisons, as they're handled by `useless_comparison`.
if matches!(value, Expr::Compare(_) | Expr::EllipsisLiteral(_)) {
if value.is_compare_expr() {
return;
}
if checker.settings.preview.is_enabled() {
if checker.semantic().in_pep_257_docstring() || checker.semantic().in_attribute_docstring()
{
return;
}
} else if matches!(value, Expr::StringLiteral(_) | Expr::FString(_)) {
// Ignore strings, to avoid false positives with docstrings.
if matches!(
value,
Expr::FString(_) | Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
) {
return;
}

View File

@@ -3,8 +3,8 @@ source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018.py:11:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
9 | "str" # StringLiteral
10 | f"{int}" # FString
9 | "str" # Str (no raise)
10 | f"{int}" # JoinedStr (no raise)
11 | 1j # Number (complex)
| ^^ B018
12 | 1 # Number (int)
@@ -13,7 +13,7 @@ B018.py:11:5: B018 Found useless expression. Either assign it to a variable or r
B018.py:12:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
10 | f"{int}" # FString
10 | f"{int}" # JoinedStr (no raise)
11 | 1j # Number (complex)
12 | 1 # Number (int)
| ^ B018
@@ -117,8 +117,8 @@ B018.py:27:5: B018 Found useless expression. Either assign it to a variable or r
B018.py:39:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
37 | "str" # StringLiteral
38 | f"{int}" # FString
37 | "str" # Str (no raise)
38 | f"{int}" # JoinedStr (no raise)
39 | 1j # Number (complex)
| ^^ B018
40 | 1 # Number (int)
@@ -127,7 +127,7 @@ B018.py:39:5: B018 Found useless expression. Either assign it to a variable or r
B018.py:40:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
38 | f"{int}" # FString
38 | f"{int}" # JoinedStr (no raise)
39 | 1j # Number (complex)
40 | 1 # Number (int)
| ^ B018
@@ -254,3 +254,5 @@ B018.py:65:5: B018 Found useless expression. Either assign it to a variable or r
65 | "foo" + "bar" # BinOp (raise)
| ^^^^^^^^^^^^^ B018
|

View File

@@ -1,11 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018_attribute_docstring.py:22:2: B018 Found useless expression. Either assign it to a variable or remove it.
|
20 | "d: not a docstring"
21 |
22 | (e := 1)
| ^^^^^^ B018
23 | "e: not a docstring"
|

View File

@@ -1,32 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018.ipynb:5:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
3 | x
4 | # Only skip the last expression
5 | x # B018
| ^ B018
6 | x
7 | # Nested expressions isn't relevant
|
B018.ipynb:9:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
7 | # Nested expressions isn't relevant
8 | if True:
9 | x
| ^ B018
10 | # Semicolons shouldn't affect the output
11 | x;
|
B018.ipynb:13:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
11 | x;
12 | # Semicolons with multiple expressions
13 | x; x
| ^ B018
14 | # Comments, newlines and whitespace
15 | x # comment
|

View File

@@ -1,295 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018.py:10:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
8 | a = 2
9 | "str" # StringLiteral
10 | f"{int}" # FString
| ^^^^^^^^ B018
11 | 1j # Number (complex)
12 | 1 # Number (int)
|
B018.py:11:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
9 | "str" # StringLiteral
10 | f"{int}" # FString
11 | 1j # Number (complex)
| ^^ B018
12 | 1 # Number (int)
13 | 1.0 # Number (float)
|
B018.py:12:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
10 | f"{int}" # FString
11 | 1j # Number (complex)
12 | 1 # Number (int)
| ^ B018
13 | 1.0 # Number (float)
14 | b"foo" # Binary
|
B018.py:13:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
11 | 1j # Number (complex)
12 | 1 # Number (int)
13 | 1.0 # Number (float)
| ^^^ B018
14 | b"foo" # Binary
15 | True # NameConstant (True)
|
B018.py:14:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
12 | 1 # Number (int)
13 | 1.0 # Number (float)
14 | b"foo" # Binary
| ^^^^^^ B018
15 | True # NameConstant (True)
16 | False # NameConstant (False)
|
B018.py:15:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
13 | 1.0 # Number (float)
14 | b"foo" # Binary
15 | True # NameConstant (True)
| ^^^^ B018
16 | False # NameConstant (False)
17 | None # NameConstant (None)
|
B018.py:16:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
14 | b"foo" # Binary
15 | True # NameConstant (True)
16 | False # NameConstant (False)
| ^^^^^ B018
17 | None # NameConstant (None)
18 | [1, 2] # list
|
B018.py:17:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
15 | True # NameConstant (True)
16 | False # NameConstant (False)
17 | None # NameConstant (None)
| ^^^^ B018
18 | [1, 2] # list
19 | {1, 2} # set
|
B018.py:18:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
16 | False # NameConstant (False)
17 | None # NameConstant (None)
18 | [1, 2] # list
| ^^^^^^ B018
19 | {1, 2} # set
20 | {"foo": "bar"} # dict
|
B018.py:19:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
17 | None # NameConstant (None)
18 | [1, 2] # list
19 | {1, 2} # set
| ^^^^^^ B018
20 | {"foo": "bar"} # dict
|
B018.py:20:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
18 | [1, 2] # list
19 | {1, 2} # set
20 | {"foo": "bar"} # dict
| ^^^^^^^^^^^^^^ B018
|
B018.py:24:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
23 | class Foo3:
24 | 123
| ^^^ B018
25 | a = 2
26 | "str"
|
B018.py:27:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
25 | a = 2
26 | "str"
27 | 1
| ^ B018
|
B018.py:37:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
35 | """my docstring"""
36 | a = 2
37 | "str" # StringLiteral
| ^^^^^ B018
38 | f"{int}" # FString
39 | 1j # Number (complex)
|
B018.py:38:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
36 | a = 2
37 | "str" # StringLiteral
38 | f"{int}" # FString
| ^^^^^^^^ B018
39 | 1j # Number (complex)
40 | 1 # Number (int)
|
B018.py:39:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
37 | "str" # StringLiteral
38 | f"{int}" # FString
39 | 1j # Number (complex)
| ^^ B018
40 | 1 # Number (int)
41 | 1.0 # Number (float)
|
B018.py:40:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
38 | f"{int}" # FString
39 | 1j # Number (complex)
40 | 1 # Number (int)
| ^ B018
41 | 1.0 # Number (float)
42 | b"foo" # Binary
|
B018.py:41:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
39 | 1j # Number (complex)
40 | 1 # Number (int)
41 | 1.0 # Number (float)
| ^^^ B018
42 | b"foo" # Binary
43 | True # NameConstant (True)
|
B018.py:42:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
40 | 1 # Number (int)
41 | 1.0 # Number (float)
42 | b"foo" # Binary
| ^^^^^^ B018
43 | True # NameConstant (True)
44 | False # NameConstant (False)
|
B018.py:43:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
41 | 1.0 # Number (float)
42 | b"foo" # Binary
43 | True # NameConstant (True)
| ^^^^ B018
44 | False # NameConstant (False)
45 | None # NameConstant (None)
|
B018.py:44:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
42 | b"foo" # Binary
43 | True # NameConstant (True)
44 | False # NameConstant (False)
| ^^^^^ B018
45 | None # NameConstant (None)
46 | [1, 2] # list
|
B018.py:45:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
43 | True # NameConstant (True)
44 | False # NameConstant (False)
45 | None # NameConstant (None)
| ^^^^ B018
46 | [1, 2] # list
47 | {1, 2} # set
|
B018.py:46:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
44 | False # NameConstant (False)
45 | None # NameConstant (None)
46 | [1, 2] # list
| ^^^^^^ B018
47 | {1, 2} # set
48 | {"foo": "bar"} # dict
|
B018.py:47:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
45 | None # NameConstant (None)
46 | [1, 2] # list
47 | {1, 2} # set
| ^^^^^^ B018
48 | {"foo": "bar"} # dict
|
B018.py:48:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
46 | [1, 2] # list
47 | {1, 2} # set
48 | {"foo": "bar"} # dict
| ^^^^^^^^^^^^^^ B018
|
B018.py:52:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
51 | def foo3():
52 | 123
| ^^^ B018
53 | a = 2
54 | "str"
|
B018.py:54:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
52 | 123
53 | a = 2
54 | "str"
| ^^^^^ B018
55 | 3
|
B018.py:55:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
53 | a = 2
54 | "str"
55 | 3
| ^ B018
|
B018.py:63:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
62 | def foo5():
63 | foo.bar # Attribute (raise)
| ^^^^^^^ B018
64 | object().__class__ # Attribute (raise)
65 | "foo" + "bar" # BinOp (raise)
|
B018.py:64:5: B018 Found useless attribute access. Either assign it to a variable or remove it.
|
62 | def foo5():
63 | foo.bar # Attribute (raise)
64 | object().__class__ # Attribute (raise)
| ^^^^^^^^^^^^^^^^^^ B018
65 | "foo" + "bar" # BinOp (raise)
|
B018.py:65:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
63 | foo.bar # Attribute (raise)
64 | object().__class__ # Attribute (raise)
65 | "foo" + "bar" # BinOp (raise)
| ^^^^^^^^^^^^^ B018
|

View File

@@ -1,165 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B018_attribute_docstring.py:10:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
8 | b = 1
9 | "b: docstring" " continue"
10 | "b: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
11 |
12 | c: int = 1
|
B018_attribute_docstring.py:20:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
18 | if True:
19 | d = 1
20 | "d: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
21 |
22 | (e := 1)
|
B018_attribute_docstring.py:22:2: B018 Found useless expression. Either assign it to a variable or remove it.
|
20 | "d: not a docstring"
21 |
22 | (e := 1)
| ^^^^^^ B018
23 | "e: not a docstring"
|
B018_attribute_docstring.py:23:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
22 | (e := 1)
23 | "e: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
24 |
25 | f = 0
|
B018_attribute_docstring.py:27:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
25 | f = 0
26 | f += 1
27 | "f: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
28 |
29 | g.h = 1
|
B018_attribute_docstring.py:30:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
29 | g.h = 1
30 | "g.h: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
31 |
32 | (i) = 1
|
B018_attribute_docstring.py:42:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
41 | l = m = 1
42 | "l m: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
43 |
44 | n.a = n.b = n.c = 1
|
B018_attribute_docstring.py:45:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
44 | n.a = n.b = n.c = 1
45 | "n.*: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
46 |
47 | (o, p) = (1, 2)
|
B018_attribute_docstring.py:48:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
47 | (o, p) = (1, 2)
48 | "o p: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
49 |
50 | [q, r] = [1, 2]
|
B018_attribute_docstring.py:51:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
50 | [q, r] = [1, 2]
51 | "q r: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^ B018
52 |
53 | *s = 1
|
B018_attribute_docstring.py:54:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
53 | *s = 1
54 | "s: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
|
B018_attribute_docstring.py:63:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
61 | b: int
62 | "Foo.b: docstring"
63 | "Foo.b: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^^^ B018
64 |
65 | c: int = 1
|
B018_attribute_docstring.py:71:9: B018 Found useless expression. Either assign it to a variable or remove it.
|
69 | # This is actually a docstring but we currently don't detect it.
70 | self.x = 1
71 | "self.x: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B018
72 |
73 | t = 2
|
B018_attribute_docstring.py:74:9: B018 Found useless expression. Either assign it to a variable or remove it.
|
73 | t = 2
74 | "t: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
75 |
76 | def random(self):
|
B018_attribute_docstring.py:78:9: B018 Found useless expression. Either assign it to a variable or remove it.
|
76 | def random(self):
77 | self.y = 2
78 | "self.y: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B018
79 |
80 | u = 2
|
B018_attribute_docstring.py:81:9: B018 Found useless expression. Either assign it to a variable or remove it.
|
80 | u = 2
81 | "u: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
82 |
83 | def add(self, y: int):
|
B018_attribute_docstring.py:89:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
87 | def function():
88 | v = 2
89 | "v: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^ B018
|
B018_attribute_docstring.py:93:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
92 | function.a = 1
93 | "function.a: not a docstring"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B018
|

View File

@@ -2,7 +2,8 @@ use ruff_diagnostics::{AlwaysFixableViolation, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_python_parser::{TokenKind, TokenKindIter};
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
@@ -51,26 +52,26 @@ impl Token {
}
}
impl From<(TokenKind, TextRange)> for Token {
fn from((tok, range): (TokenKind, TextRange)) -> Self {
impl From<(&Tok, TextRange)> for Token {
fn from((tok, range): (&Tok, TextRange)) -> Self {
let ty = match tok {
TokenKind::Name => TokenType::Named,
TokenKind::String => TokenType::String,
TokenKind::Newline => TokenType::Newline,
TokenKind::NonLogicalNewline => TokenType::NonLogicalNewline,
TokenKind::Lpar => TokenType::OpeningBracket,
TokenKind::Rpar => TokenType::ClosingBracket,
TokenKind::Lsqb => TokenType::OpeningSquareBracket,
TokenKind::Rsqb => TokenType::ClosingBracket,
TokenKind::Colon => TokenType::Colon,
TokenKind::Comma => TokenType::Comma,
TokenKind::Lbrace => TokenType::OpeningCurlyBracket,
TokenKind::Rbrace => TokenType::ClosingBracket,
TokenKind::Def => TokenType::Def,
TokenKind::For => TokenType::For,
TokenKind::Lambda => TokenType::Lambda,
Tok::Name { .. } => TokenType::Named,
Tok::String { .. } => TokenType::String,
Tok::Newline => TokenType::Newline,
Tok::NonLogicalNewline => TokenType::NonLogicalNewline,
Tok::Lpar => TokenType::OpeningBracket,
Tok::Rpar => TokenType::ClosingBracket,
Tok::Lsqb => TokenType::OpeningSquareBracket,
Tok::Rsqb => TokenType::ClosingBracket,
Tok::Colon => TokenType::Colon,
Tok::Comma => TokenType::Comma,
Tok::Lbrace => TokenType::OpeningCurlyBracket,
Tok::Rbrace => TokenType::ClosingBracket,
Tok::Def => TokenType::Def,
Tok::For => TokenType::For,
Tok::Lambda => TokenType::Lambda,
// Import treated like a function.
TokenKind::Import => TokenType::Named,
Tok::Import => TokenType::Named,
_ => TokenType::Irrelevant,
};
#[allow(clippy::inconsistent_struct_constructor)]
@@ -226,23 +227,27 @@ impl AlwaysFixableViolation for ProhibitedTrailingComma {
/// COM812, COM818, COM819
pub(crate) fn trailing_commas(
diagnostics: &mut Vec<Diagnostic>,
tokens: TokenKindIter,
tokens: &[LexResult],
locator: &Locator,
indexer: &Indexer,
) {
let mut fstrings = 0u32;
let tokens = tokens.filter_map(|(token, tok_range)| {
match token {
let tokens = tokens.iter().filter_map(|result| {
let Ok((tok, tok_range)) = result else {
return None;
};
match tok {
// Completely ignore comments -- they just interfere with the logic.
TokenKind::Comment => None,
Tok::Comment(_) => None,
// F-strings are handled as `String` token type with the complete range
// of the outermost f-string. This means that the expression inside the
// f-string is not checked for trailing commas.
TokenKind::FStringStart => {
Tok::FStringStart(_) => {
fstrings = fstrings.saturating_add(1);
None
}
TokenKind::FStringEnd => {
Tok::FStringEnd => {
fstrings = fstrings.saturating_sub(1);
if fstrings == 0 {
indexer
@@ -255,7 +260,7 @@ pub(crate) fn trailing_commas(
}
_ => {
if fstrings == 0 {
Some(Token::from((token, tok_range)))
Some(Token::from((tok, *tok_range)))
} else {
None
}

View File

@@ -118,11 +118,7 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &mut Checker, call: &
}
}
ast::FStringPart::FString(f_string) => {
if f_string
.elements
.literals()
.any(|literal| literal.contains("%z"))
{
if f_string.literals().any(|literal| literal.contains("%z")) {
return;
}
}

View File

@@ -4,9 +4,10 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_index::Indexer;
use ruff_python_parser::{TokenKind, TokenKindIter};
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_source_file::Locator;
use ruff_text_size::TextRange;
use ruff_text_size::{Ranged, TextRange};
use crate::settings::LinterSettings;
@@ -92,34 +93,36 @@ impl Violation for MultiLineImplicitStringConcatenation {
/// ISC001, ISC002
pub(crate) fn implicit(
diagnostics: &mut Vec<Diagnostic>,
tokens: TokenKindIter,
tokens: &[LexResult],
settings: &LinterSettings,
locator: &Locator,
indexer: &Indexer,
) {
for ((a_tok, a_range), (b_tok, b_range)) in tokens
.filter(|(token, _)| {
*token != TokenKind::Comment
.iter()
.flatten()
.filter(|(tok, _)| {
!tok.is_comment()
&& (settings.flake8_implicit_str_concat.allow_multiline
|| *token != TokenKind::NonLogicalNewline)
|| !tok.is_non_logical_newline())
})
.tuple_windows()
{
let (a_range, b_range) = match (a_tok, b_tok) {
(TokenKind::String, TokenKind::String) => (a_range, b_range),
(TokenKind::String, TokenKind::FStringStart) => {
(Tok::String { .. }, Tok::String { .. }) => (*a_range, *b_range),
(Tok::String { .. }, Tok::FStringStart(_)) => {
match indexer.fstring_ranges().innermost(b_range.start()) {
Some(b_range) => (a_range, b_range),
Some(b_range) => (*a_range, b_range),
None => continue,
}
}
(TokenKind::FStringEnd, TokenKind::String) => {
(Tok::FStringEnd, Tok::String { .. }) => {
match indexer.fstring_ranges().innermost(a_range.start()) {
Some(a_range) => (a_range, b_range),
Some(a_range) => (a_range, *b_range),
None => continue,
}
}
(TokenKind::FStringEnd, TokenKind::FStringStart) => {
(Tok::FStringEnd, Tok::FStringStart(_)) => {
match (
indexer.fstring_ranges().innermost(a_range.start()),
indexer.fstring_ranges().innermost(b_range.start()),

View File

@@ -1,10 +1,10 @@
use ruff_python_ast::AnyStringFlags;
use ruff_python_ast::AnyStringKind;
use ruff_text_size::TextLen;
/// Returns the raw contents of the string given the string's contents and flags.
/// Returns the raw contents of the string given the string's contents and kind.
/// This is a string without the prefix and quotes.
pub(super) fn raw_contents(contents: &str, flags: AnyStringFlags) -> &str {
&contents[flags.opener_len().to_usize()..(contents.text_len() - flags.closer_len()).to_usize()]
pub(super) fn raw_contents(contents: &str, kind: AnyStringKind) -> &str {
&contents[kind.opener_len().to_usize()..(contents.text_len() - kind.closer_len()).to_usize()]
}
/// Return `true` if the haystack contains an escaped quote.

View File

@@ -1,7 +1,7 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::visitor::{walk_f_string, Visitor};
use ruff_python_ast::{self as ast, AnyStringFlags, StringLike};
use ruff_python_ast::{self as ast, AnyStringKind, StringLike};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -110,7 +110,7 @@ impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_> {
self.locator,
self.quotes_settings,
string_literal.range(),
AnyStringFlags::from(string_literal.flags),
AnyStringKind::from(string_literal.flags),
) {
self.diagnostics.push(diagnostic);
}
@@ -121,7 +121,7 @@ impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_> {
self.locator,
self.quotes_settings,
bytes_literal.range(),
AnyStringFlags::from(bytes_literal.flags),
AnyStringKind::from(bytes_literal.flags),
) {
self.diagnostics.push(diagnostic);
}
@@ -192,7 +192,6 @@ impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_> {
// f"'normal' {f'\'nested\' {x} "double quotes"'} normal"
// ```
if !f_string
.elements
.literals()
.any(|literal| contains_quote(literal, opposite_quote_char))
{
@@ -214,20 +213,20 @@ fn check_string_or_bytes(
locator: &Locator,
quotes_settings: &flake8_quotes::settings::Settings,
range: TextRange,
flags: AnyStringFlags,
kind: AnyStringKind,
) -> Option<Diagnostic> {
assert!(!flags.is_f_string());
assert!(!kind.is_f_string());
if flags.is_triple_quoted() || flags.is_raw_string() {
if kind.is_triple_quoted() || kind.is_raw_string() {
return None;
}
// Check if we're using the preferred quotation style.
if Quote::from(flags.quote_style()) != quotes_settings.inline_quotes {
if Quote::from(kind.quote_style()) != quotes_settings.inline_quotes {
return None;
}
let contents = raw_contents(locator.slice(range), flags);
let contents = raw_contents(locator.slice(range), kind);
if !contains_escaped_quote(contents, quotes_settings.inline_quotes.as_char())
|| contains_quote(contents, quotes_settings.inline_quotes.opposite().as_char())
@@ -238,7 +237,7 @@ fn check_string_or_bytes(
let mut diagnostic = Diagnostic::new(AvoidableEscapedQuote, range);
let fixed_contents = format!(
"{prefix}{quote}{value}{quote}",
prefix = flags.prefix(),
prefix = kind.prefix(),
quote = quotes_settings.inline_quotes.opposite().as_char(),
value = unescape_string(contents, quotes_settings.inline_quotes.as_char())
);
@@ -270,7 +269,7 @@ fn check_f_string(
let opposite_quote_char = quotes_settings.inline_quotes.opposite().as_char();
let mut edits = vec![];
for literal in f_string.elements.literals() {
for literal in f_string.literals() {
let content = locator.slice(literal);
if !contains_escaped_quote(content, quote_char) {
continue;

View File

@@ -454,7 +454,10 @@ pub(crate) fn check_string_quotes(checker: &mut Checker, string_like: StringLike
return;
}
let ranges: Vec<_> = string_like.parts().map(|part| part.range()).collect();
let ranges = string_like
.parts()
.map(|part| part.range())
.collect::<Vec<_>>();
if checker.semantic().in_pep_257_docstring() {
if checker.enabled(Rule::BadQuotesDocstring) {

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, AnyStringFlags, StringLike};
use ruff_python_ast::{self as ast, AnyStringKind, StringLike};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
@@ -59,7 +59,7 @@ pub(crate) fn unnecessary_escaped_quote(checker: &mut Checker, string_like: Stri
if let Some(diagnostic) = check_string_or_bytes(
locator,
string_literal.range(),
AnyStringFlags::from(string_literal.flags),
AnyStringKind::from(string_literal.flags),
) {
checker.diagnostics.push(diagnostic);
}
@@ -68,7 +68,7 @@ pub(crate) fn unnecessary_escaped_quote(checker: &mut Checker, string_like: Stri
if let Some(diagnostic) = check_string_or_bytes(
locator,
bytes_literal.range(),
AnyStringFlags::from(bytes_literal.flags),
AnyStringKind::from(bytes_literal.flags),
) {
checker.diagnostics.push(diagnostic);
}
@@ -90,16 +90,16 @@ pub(crate) fn unnecessary_escaped_quote(checker: &mut Checker, string_like: Stri
fn check_string_or_bytes(
locator: &Locator,
range: TextRange,
flags: AnyStringFlags,
kind: AnyStringKind,
) -> Option<Diagnostic> {
assert!(!flags.is_f_string());
assert!(!kind.is_f_string());
if flags.is_triple_quoted() || flags.is_raw_string() {
if kind.is_triple_quoted() || kind.is_raw_string() {
return None;
}
let contents = raw_contents(locator.slice(range), flags);
let quote = flags.quote_style();
let contents = raw_contents(locator.slice(range), kind);
let quote = kind.quote_style();
let opposite_quote_char = quote.opposite().as_char();
if !contains_escaped_quote(contents, opposite_quote_char) {
@@ -108,7 +108,7 @@ fn check_string_or_bytes(
let mut diagnostic = Diagnostic::new(UnnecessaryEscapedQuote, range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
flags.format_string_contents(&unescape_string(contents, opposite_quote_char)),
kind.format_string_contents(&unescape_string(contents, opposite_quote_char)),
range,
)));
Some(diagnostic)
@@ -124,7 +124,7 @@ fn check_f_string(locator: &Locator, f_string: &ast::FString) -> Option<Diagnost
let opposite_quote_char = flags.quote_style().opposite().as_char();
let mut edits = vec![];
for literal in f_string.elements.literals() {
for literal in f_string.literals() {
let content = locator.slice(literal);
if !contains_escaped_quote(content, opposite_quote_char) {
continue;

View File

@@ -1,6 +1,5 @@
use ruff_python_ast::{
self as ast, str_prefix::StringLiteralPrefix, Arguments, Expr, StringLiteralFlags,
};
use ast::{StringLiteralFlags, StringLiteralPrefix};
use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_text_size::Ranged;
use crate::fix::snippet::SourceCodeSnippet;

View File

@@ -1,5 +1,4 @@
use anyhow::Result;
use std::cmp::Reverse;
use ruff_diagnostics::Edit;
use ruff_python_ast::helpers::{map_callable, map_subscript};
@@ -287,17 +286,11 @@ pub(crate) fn quote_annotation(
/// Filter out any [`Edit`]s that are completely contained by any other [`Edit`].
pub(crate) fn filter_contained(edits: Vec<Edit>) -> Vec<Edit> {
let mut edits = edits;
// Sort such that the largest edits are prioritized.
edits.sort_unstable_by_key(|edit| (edit.start(), Reverse(edit.end())));
// Remove any edits that are completely contained by another edit.
let mut filtered: Vec<Edit> = Vec::with_capacity(edits.len());
for edit in edits {
if !filtered
if filtered
.iter()
.any(|filtered_edit| filtered_edit.range().contains_range(edit.range()))
.all(|filtered_edit| !filtered_edit.range().contains_range(edit.range()))
{
filtered.push(edit);
}

View File

@@ -296,8 +296,6 @@ quote.py:78:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a typ
91 |- def func() -> DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]:
91 |+ def func() -> "DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]":
92 92 | ...
93 93 |
94 94 |
quote.py:78:35: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
|
@@ -339,61 +337,5 @@ quote.py:78:35: TCH002 [*] Move third-party import `pandas.Series` into a type-c
91 |- def func() -> DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]:
91 |+ def func() -> "DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]":
92 92 | ...
93 93 |
94 94 |
quote.py:96:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
95 | def f():
96 | from pandas import DataFrame, Series
| ^^^^^^^^^ TCH002
97 |
98 | def func(self) -> DataFrame | list[Series]:
|
= help: Move into type-checking block
Unsafe fix
1 |+from typing import TYPE_CHECKING
2 |+
3 |+if TYPE_CHECKING:
4 |+ from pandas import DataFrame, Series
1 5 | def f():
2 6 | from pandas import DataFrame
3 7 |
--------------------------------------------------------------------------------
93 97 |
94 98 |
95 99 | def f():
96 |- from pandas import DataFrame, Series
97 100 |
98 |- def func(self) -> DataFrame | list[Series]:
101 |+ def func(self) -> "DataFrame | list[Series]":
99 102 | pass
quote.py:96:35: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
|
95 | def f():
96 | from pandas import DataFrame, Series
| ^^^^^^ TCH002
97 |
98 | def func(self) -> DataFrame | list[Series]:
|
= help: Move into type-checking block
Unsafe fix
1 |+from typing import TYPE_CHECKING
2 |+
3 |+if TYPE_CHECKING:
4 |+ from pandas import DataFrame, Series
1 5 | def f():
2 6 | from pandas import DataFrame
3 7 |
--------------------------------------------------------------------------------
93 97 |
94 98 |
95 99 | def f():
96 |- from pandas import DataFrame, Series
97 100 |
98 |- def func(self) -> DataFrame | list[Series]:
101 |+ def func(self) -> "DataFrame | list[Series]":
99 102 | pass

View File

@@ -96,7 +96,7 @@ fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option<Expr> {
}
let node = ast::FString {
elements: f_string_elements.into(),
elements: f_string_elements,
range: TextRange::default(),
flags: FStringFlags::default(),
};

View File

@@ -12,7 +12,10 @@ use ruff_diagnostics::Fix;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_parser::{TokenKind, TokenKindIter};
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::lexer::LexicalError;
use ruff_python_parser::Tok;
use ruff_python_parser::TokenKind;
use ruff_source_file::{Locator, UniversalNewlines};
use ruff_text_size::TextRange;
use ruff_text_size::TextSize;
@@ -381,7 +384,7 @@ struct LogicalLineInfo {
/// Iterator that processes tokens until a full logical line (or comment line) is "built".
/// It then returns characteristics of that logical line (see `LogicalLineInfo`).
struct LinePreprocessor<'a> {
tokens: TokenKindIter<'a>,
tokens: Iter<'a, Result<(Tok, TextRange), LexicalError>>,
locator: &'a Locator<'a>,
indent_width: IndentWidth,
/// The start position of the next logical line.
@@ -397,13 +400,13 @@ struct LinePreprocessor<'a> {
impl<'a> LinePreprocessor<'a> {
fn new(
tokens: TokenKindIter<'a>,
tokens: &'a [LexResult],
locator: &'a Locator,
indent_width: IndentWidth,
cell_offsets: Option<&'a CellOffsets>,
) -> LinePreprocessor<'a> {
LinePreprocessor {
tokens,
tokens: tokens.iter(),
locator,
line_start: TextSize::new(0),
max_preceding_blank_lines: BlankLines::Zero,
@@ -427,11 +430,17 @@ impl<'a> Iterator for LinePreprocessor<'a> {
let mut last_token: TokenKind = TokenKind::EndOfFile;
let mut parens = 0u32;
while let Some((token, range)) = self.tokens.next() {
if matches!(token, TokenKind::Indent | TokenKind::Dedent) {
while let Some(result) = self.tokens.next() {
let Ok((token, range)) = result else {
continue;
};
if matches!(token, Tok::Indent | Tok::Dedent) {
continue;
}
let token_kind = TokenKind::from_token(token);
let (logical_line_kind, first_token_range) = if let Some(first_token_range) =
first_logical_line_token
{
@@ -453,23 +462,25 @@ impl<'a> Iterator for LinePreprocessor<'a> {
}
// An empty line
if token == TokenKind::NonLogicalNewline {
blank_lines.add(range);
if token_kind == TokenKind::NonLogicalNewline {
blank_lines.add(*range);
self.line_start = range.end();
continue;
}
is_docstring = token == TokenKind::String;
is_docstring = token_kind == TokenKind::String;
let logical_line_kind = match token {
let logical_line_kind = match token_kind {
TokenKind::Class => LogicalLineKind::Class,
TokenKind::Comment => LogicalLineKind::Comment,
TokenKind::At => LogicalLineKind::Decorator,
TokenKind::Def => LogicalLineKind::Function,
// Lookahead to distinguish `async def` from `async with`.
TokenKind::Async if matches!(self.tokens.peek(), Some((TokenKind::Def, _))) => {
TokenKind::Async
if matches!(self.tokens.as_slice().first(), Some(Ok((Tok::Def, _)))) =>
{
LogicalLineKind::Function
}
TokenKind::Import => LogicalLineKind::Import,
@@ -477,22 +488,22 @@ impl<'a> Iterator for LinePreprocessor<'a> {
_ => LogicalLineKind::Other,
};
first_logical_line_token = Some((logical_line_kind, range));
first_logical_line_token = Some((logical_line_kind, *range));
(logical_line_kind, range)
(logical_line_kind, *range)
};
if !token.is_trivia() {
if !token_kind.is_trivia() {
line_is_comment_only = false;
}
// A docstring line is composed only of the docstring (TokenKind::String) and trivia tokens.
// (If a comment follows a docstring, we still count the line as a docstring)
if token != TokenKind::String && !token.is_trivia() {
if token_kind != TokenKind::String && !token_kind.is_trivia() {
is_docstring = false;
}
match token {
match token_kind {
TokenKind::Lbrace | TokenKind::Lpar | TokenKind::Lsqb => {
parens = parens.saturating_add(1);
}
@@ -538,8 +549,8 @@ impl<'a> Iterator for LinePreprocessor<'a> {
_ => {}
}
if !token.is_trivia() {
last_token = token;
if !token_kind.is_trivia() {
last_token = token_kind;
}
}
@@ -722,7 +733,7 @@ impl<'a> BlankLinesChecker<'a> {
}
/// E301, E302, E303, E304, E305, E306
pub(crate) fn check_lines(&self, tokens: TokenKindIter<'a>, diagnostics: &mut Vec<Diagnostic>) {
pub(crate) fn check_lines(&self, tokens: &[LexResult], diagnostics: &mut Vec<Diagnostic>) {
let mut prev_indent_length: Option<usize> = None;
let mut state = BlankLinesState::default();
let line_preprocessor =

View File

@@ -1,6 +1,7 @@
use ruff_notebook::CellOffsets;
use ruff_python_ast::PySourceType;
use ruff_python_parser::{TokenKind, TokenKindIter};
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{AlwaysFixableViolation, Violation};
@@ -99,7 +100,7 @@ impl AlwaysFixableViolation for UselessSemicolon {
/// E701, E702, E703
pub(crate) fn compound_statements(
diagnostics: &mut Vec<Diagnostic>,
mut tokens: TokenKindIter,
lxr: &[LexResult],
locator: &Locator,
indexer: &Indexer,
source_type: PySourceType,
@@ -133,36 +134,39 @@ pub(crate) fn compound_statements(
// Track indentation.
let mut indent = 0u32;
while let Some((token, range)) = tokens.next() {
match token {
TokenKind::Lpar => {
// Keep the token iterator to perform lookaheads.
let mut tokens = lxr.iter().flatten();
while let Some(&(ref tok, range)) = tokens.next() {
match tok {
Tok::Lpar => {
par_count = par_count.saturating_add(1);
}
TokenKind::Rpar => {
Tok::Rpar => {
par_count = par_count.saturating_sub(1);
}
TokenKind::Lsqb => {
Tok::Lsqb => {
sqb_count = sqb_count.saturating_add(1);
}
TokenKind::Rsqb => {
Tok::Rsqb => {
sqb_count = sqb_count.saturating_sub(1);
}
TokenKind::Lbrace => {
Tok::Lbrace => {
brace_count = brace_count.saturating_add(1);
}
TokenKind::Rbrace => {
Tok::Rbrace => {
brace_count = brace_count.saturating_sub(1);
}
TokenKind::Ellipsis => {
Tok::Ellipsis => {
if allow_ellipsis {
allow_ellipsis = false;
continue;
}
}
TokenKind::Indent => {
Tok::Indent => {
indent = indent.saturating_add(1);
}
TokenKind::Dedent => {
Tok::Dedent => {
indent = indent.saturating_sub(1);
}
_ => {}
@@ -172,8 +176,8 @@ pub(crate) fn compound_statements(
continue;
}
match token {
TokenKind::Newline => {
match tok {
Tok::Newline => {
if let Some((start, end)) = semi {
if !(source_type.is_ipynb()
&& indent == 0
@@ -211,7 +215,7 @@ pub(crate) fn compound_statements(
while_ = None;
with = None;
}
TokenKind::Colon => {
Tok::Colon => {
if case.is_some()
|| class.is_some()
|| elif.is_some()
@@ -231,14 +235,11 @@ pub(crate) fn compound_statements(
allow_ellipsis = true;
}
}
TokenKind::Semi => {
Tok::Semi => {
semi = Some((range.start(), range.end()));
allow_ellipsis = false;
}
TokenKind::Comment
| TokenKind::Indent
| TokenKind::Dedent
| TokenKind::NonLogicalNewline => {}
Tok::Comment(..) | Tok::Indent | Tok::Dedent | Tok::NonLogicalNewline => {}
_ => {
if let Some((start, end)) = semi {
diagnostics.push(Diagnostic::new(
@@ -276,8 +277,8 @@ pub(crate) fn compound_statements(
}
}
match token {
TokenKind::Lambda => {
match tok {
Tok::Lambda => {
// Reset.
colon = None;
case = None;
@@ -293,40 +294,40 @@ pub(crate) fn compound_statements(
while_ = None;
with = None;
}
TokenKind::Case => {
Tok::Case => {
case = Some((range.start(), range.end()));
}
TokenKind::If => {
Tok::If => {
if_ = Some((range.start(), range.end()));
}
TokenKind::While => {
Tok::While => {
while_ = Some((range.start(), range.end()));
}
TokenKind::For => {
Tok::For => {
for_ = Some((range.start(), range.end()));
}
TokenKind::Try => {
Tok::Try => {
try_ = Some((range.start(), range.end()));
}
TokenKind::Except => {
Tok::Except => {
except = Some((range.start(), range.end()));
}
TokenKind::Finally => {
Tok::Finally => {
finally = Some((range.start(), range.end()));
}
TokenKind::Elif => {
Tok::Elif => {
elif = Some((range.start(), range.end()));
}
TokenKind::Else => {
Tok::Else => {
else_ = Some((range.start(), range.end()));
}
TokenKind::Class => {
Tok::Class => {
class = Some((range.start(), range.end()));
}
TokenKind::With => {
Tok::With => {
with = Some((range.start(), range.end()));
}
TokenKind::Match => {
Tok::Match => {
match_ = Some((range.start(), range.end()));
}
_ => {}
@@ -336,17 +337,17 @@ pub(crate) fn compound_statements(
/// Returns `true` if there are any non-trivia tokens from the given token
/// iterator till the given end offset.
fn has_non_trivia_tokens_till(tokens: TokenKindIter, cell_end: TextSize) -> bool {
for (token, tok_range) in tokens {
fn has_non_trivia_tokens_till<'a>(
tokens: impl Iterator<Item = &'a (Tok, TextRange)>,
cell_end: TextSize,
) -> bool {
for &(ref tok, tok_range) in tokens {
if tok_range.start() >= cell_end {
return false;
}
if !matches!(
token,
TokenKind::Newline
| TokenKind::Comment
| TokenKind::EndOfFile
| TokenKind::NonLogicalNewline
tok,
Tok::Newline | Tok::Comment(_) | Tok::EndOfFile | Tok::NonLogicalNewline
) {
return true;
}

View File

@@ -2,11 +2,11 @@ use memchr::memchr_iter;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{AnyStringFlags, FStringElement, StringLike, StringLikePart};
use ruff_python_index::Indexer;
use ruff_python_parser::Tok;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::checkers::ast::Checker;
use crate::fix::edits::pad_start;
/// ## What it does
@@ -59,83 +59,38 @@ impl AlwaysFixableViolation for InvalidEscapeSequence {
}
/// W605
pub(crate) fn invalid_escape_sequence(checker: &mut Checker, string_like: StringLike) {
let locator = checker.locator();
for part in string_like.parts() {
if part.flags().is_raw_string() {
continue;
}
match part {
StringLikePart::String(string_literal) => {
check(
&mut checker.diagnostics,
locator,
string_literal.start(),
string_literal.range(),
AnyStringFlags::from(string_literal.flags),
);
}
StringLikePart::Bytes(bytes_literal) => {
check(
&mut checker.diagnostics,
locator,
bytes_literal.start(),
bytes_literal.range(),
AnyStringFlags::from(bytes_literal.flags),
);
}
StringLikePart::FString(f_string) => {
let flags = AnyStringFlags::from(f_string.flags);
for element in &f_string.elements {
match element {
FStringElement::Literal(literal) => {
check(
&mut checker.diagnostics,
locator,
f_string.start(),
literal.range(),
flags,
);
}
FStringElement::Expression(expression) => {
let Some(format_spec) = expression.format_spec.as_ref() else {
continue;
};
for literal in format_spec.elements.literals() {
check(
&mut checker.diagnostics,
locator,
f_string.start(),
literal.range(),
flags,
);
}
}
}
}
}
}
}
}
fn check(
pub(crate) fn invalid_escape_sequence(
diagnostics: &mut Vec<Diagnostic>,
locator: &Locator,
// Start position of the expression that contains the source range. This is used to generate
// the fix when the source range is part of the expression like in f-string which contains
// other f-string literal elements.
expr_start: TextSize,
// Range in the source code to perform the check on.
source_range: TextRange,
flags: AnyStringFlags,
indexer: &Indexer,
token: &Tok,
token_range: TextRange,
) {
let source = locator.slice(source_range);
let (token_source_code, string_start_location, kind) = match token {
Tok::FStringMiddle { kind, .. } => {
if kind.is_raw_string() {
return;
}
let Some(f_string_range) = indexer.fstring_ranges().innermost(token_range.start())
else {
return;
};
(locator.slice(token_range), f_string_range.start(), kind)
}
Tok::String { kind, .. } => {
if kind.is_raw_string() {
return;
}
(locator.slice(token_range), token_range.start(), kind)
}
_ => return,
};
let mut contains_valid_escape_sequence = false;
let mut invalid_escape_chars = Vec::new();
let mut prev = None;
let bytes = source.as_bytes();
let bytes = token_source_code.as_bytes();
for i in memchr_iter(b'\\', bytes) {
// If the previous character was also a backslash, skip.
if prev.is_some_and(|prev| prev == i - 1) {
@@ -145,9 +100,9 @@ fn check(
prev = Some(i);
let next_char = match source[i + 1..].chars().next() {
let next_char = match token_source_code[i + 1..].chars().next() {
Some(next_char) => next_char,
None if flags.is_f_string() => {
None if token.is_f_string_middle() => {
// If we're at the end of a f-string middle token, the next character
// is actually emitted as a different token. For example,
//
@@ -169,7 +124,7 @@ fn check(
// f-string. This means that if there's a `FStringMiddle` token and we
// encounter a `\` character, then the next character is always going to
// be part of the f-string.
if let Some(next_char) = locator.after(source_range.end()).chars().next() {
if let Some(next_char) = locator.after(token_range.end()).chars().next() {
next_char
} else {
continue;
@@ -181,7 +136,6 @@ fn check(
// If we're at the end of line, skip.
if matches!(next_char, '\n' | '\r') {
contains_valid_escape_sequence = true;
continue;
}
@@ -218,7 +172,7 @@ fn check(
continue;
}
let location = source_range.start() + TextSize::try_from(i).unwrap();
let location = token_range.start() + TextSize::try_from(i).unwrap();
let range = TextRange::at(location, next_char.text_len() + TextSize::from(1));
invalid_escape_chars.push(InvalidEscapeChar {
ch: next_char,
@@ -226,6 +180,7 @@ fn check(
});
}
let mut invalid_escape_sequence = Vec::new();
if contains_valid_escape_sequence {
// Escape with backslash.
for invalid_escape_char in &invalid_escape_chars {
@@ -240,7 +195,7 @@ fn check(
r"\".to_string(),
invalid_escape_char.start() + TextSize::from(1),
)));
diagnostics.push(diagnostic);
invalid_escape_sequence.push(diagnostic);
}
} else {
// Turn into raw string.
@@ -253,12 +208,12 @@ fn check(
invalid_escape_char.range(),
);
if flags.is_u_string() {
if kind.is_u_string() {
// Replace the Unicode prefix with `r`.
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
"r".to_string(),
expr_start,
expr_start + TextSize::from(1),
string_start_location,
string_start_location + TextSize::from(1),
)));
} else {
// Insert the `r` prefix.
@@ -267,15 +222,17 @@ fn check(
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
// `returnr"foo"` is not.
Fix::safe_edit(Edit::insertion(
pad_start("r".to_string(), expr_start, locator),
expr_start,
pad_start("r".to_string(), string_start_location, locator),
string_start_location,
)),
);
}
diagnostics.push(diagnostic);
invalid_escape_sequence.push(diagnostic);
}
}
diagnostics.extend(invalid_escape_sequence);
}
#[derive(Debug, PartialEq, Eq)]

View File

@@ -198,7 +198,9 @@ pub(crate) fn missing_whitespace_around_operator(
matches!(
prev_kind,
TokenKind::Rpar | TokenKind::Rsqb | TokenKind::Rbrace
) || !(prev_kind.is_operator() || prev_kind.is_keyword())
) || !(prev_kind.is_operator()
|| prev_kind.is_keyword()
|| prev_kind.is_soft_keyword())
};
if is_binary {

View File

@@ -11,6 +11,18 @@ use ruff_text_size::{TextRange, TextSize};
/// According to [PEP 8], spaces are preferred over tabs (unless used to remain
/// consistent with code that is already indented with tabs).
///
/// ## Example
/// ```python
/// if True:
/// a = 1
/// ```
///
/// Use instead:
/// ```python
/// if True:
/// a = 1
/// ```
///
/// ## Formatter compatibility
/// We recommend against using this rule alongside the [formatter]. The
/// formatter enforces consistent indentation, making the rule redundant.

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_parser::{TokenKind, TokenKindIter};
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_text_size::{TextRange, TextSize};
/// ## What it does
@@ -56,23 +57,23 @@ impl AlwaysFixableViolation for TooManyNewlinesAtEndOfFile {
/// W391
pub(crate) fn too_many_newlines_at_end_of_file(
diagnostics: &mut Vec<Diagnostic>,
tokens: TokenKindIter,
lxr: &[LexResult],
) {
let mut num_trailing_newlines = 0u32;
let mut start: Option<TextSize> = None;
let mut end: Option<TextSize> = None;
// Count the number of trailing newlines.
for (token, range) in tokens.rev() {
match token {
TokenKind::NonLogicalNewline | TokenKind::Newline => {
for (tok, range) in lxr.iter().rev().flatten() {
match tok {
Tok::NonLogicalNewline | Tok::Newline => {
if num_trailing_newlines == 0 {
end = Some(range.end());
}
start = Some(range.end());
num_trailing_newlines += 1;
}
TokenKind::Dedent => continue,
Tok::Dedent => continue,
_ => {
break;
}

View File

@@ -190,22 +190,4 @@ E27.py:35:14: E271 [*] Multiple spaces after keyword
37 37 | from w import(e, f)
38 38 | #: E275
E27.py:70:5: E271 [*] Multiple spaces after keyword
|
69 | #: E271
70 | type Number = int
| ^^ E271
71 |
72 | #: E273
|
= help: Replace with single space
Safe fix
67 67 | # Soft keywords
68 68 |
69 69 | #: E271
70 |-type Number = int
70 |+type Number = int
71 71 |
72 72 | #: E273
73 73 | type Number = int

View File

@@ -106,22 +106,4 @@ E27.py:30:10: E273 [*] Tab after keyword
32 32 | from u import (a, b)
33 33 | from v import c, d
E27.py:73:5: E273 [*] Tab after keyword
|
72 | #: E273
73 | type Number = int
| ^^^^ E273
74 |
75 | #: E275
|
= help: Replace with single space
Safe fix
70 70 | type Number = int
71 71 |
72 72 | #: E273
73 |-type Number = int
73 |+type Number = int
74 74 |
75 75 | #: E275
76 76 | match(foo):

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