Compare commits

...

51 Commits

Author SHA1 Message Date
Charlie Marsh
d8bb0632c5 Battling allocator 2022-10-01 17:14:16 -04:00
Charlie Marsh
d7412af996 Try lifetimes 2022-09-02 19:50:15 -04:00
Charlie Marsh
b1f734f445 Little demo 2022-09-02 18:39:51 -04:00
Charlie Marsh
a9d1d17eac Try to support transformations 2022-09-02 18:18:32 -04:00
Charlie Marsh
0cfa2d617a Iterate on CST traversal 2022-09-02 16:48:07 -04:00
Charlie Marsh
21d39a9b44 Try to extend visitor 2022-09-02 11:29:48 -04:00
Charlie Marsh
9ad82a6127 Autofix-only 2022-09-02 11:29:48 -04:00
Charlie Marsh
4617c997bb Experiment with LibCST parsing 2022-09-02 11:29:47 -04:00
Charlie Marsh
26e1f4b6df Bump version to 0.0.24 2022-09-02 10:18:40 -04:00
Charlie Marsh
17c08523dc Remove rogue println 2022-09-02 10:18:12 -04:00
Charlie Marsh
221f4304ad Add support for __all__ export bindings (#87) 2022-09-02 10:17:31 -04:00
Charlie Marsh
c0131e65e5 Avoid putting decorators in the function scope (#86) 2022-09-02 09:13:06 -04:00
Charlie Marsh
bf4722a62f Fix future-to-__future__ typo (#85) 2022-09-02 08:56:26 -04:00
Charlie Marsh
994f5d452c Update .gitignore 2022-09-01 20:32:32 -04:00
Nikita Sobolev
741857cdf9 Use the latest version of actions/checkout (#79) 2022-09-01 13:18:31 -04:00
Ariel Richtman
4f42f51bd2 Add pre-commit hook (#55) 2022-09-01 13:01:28 -04:00
Dmitry Dygalo
5a3092e805 perf: Compile Regex once (#77) 2022-09-01 12:49:29 -04:00
Charlie Marsh
0318406535 Re-sort lint rules 2022-09-01 12:39:25 -04:00
Sekky61
0c99b5aac5 Fix F832 --> F823 typo (#73) 2022-09-01 12:37:34 -04:00
Kian-Meng Ang
b442402b13 Prettify md/yaml files (#74) 2022-09-01 12:36:47 -04:00
Charlie Marsh
ba27e50164 Bump version to 0.0.23 2022-09-01 09:21:43 -04:00
Charlie Marsh
d55651d470 Rename cache to .ruff_cache 2022-09-01 09:19:32 -04:00
Charlie Marsh
0c110bcecf Add some user-visible logging when pyproject.toml fails to parse 2022-09-01 09:18:47 -04:00
Patrick Haller
9bf8a0d0f1 Check if toml file is parsed correctly (#71) 2022-09-01 09:15:07 -04:00
Eric Hagman
888cfeba35 Remove .to_string() from F634 error message (#67) 2022-09-01 09:08:48 -04:00
Charlie Marsh
64df4eb311 Bump version to 0.0.22 2022-08-31 19:12:31 -04:00
Charlie Marsh
2bac3027a5 Implement F704 (#66) 2022-08-31 19:11:59 -04:00
Charlie Marsh
c28ac75591 Implement F841 (for functions) (#65) 2022-08-31 19:11:26 -04:00
Charlie Marsh
59f009b52d Enable globs in excludes list (#64) 2022-08-31 18:53:13 -04:00
Charlie Marsh
b5edcee9f2 Implement F841 (for exception handlers) (#63) 2022-08-31 18:40:34 -04:00
Charlie Marsh
3f739214b4 Bind excepthandler names (#62) 2022-08-31 18:16:37 -04:00
Charlie Marsh
0b9e3f8b47 Minor formatting changes 2022-08-31 12:38:23 -04:00
Charlie Marsh
556ae00078 Bump version to 0.0.21 2022-08-31 11:25:46 -04:00
Charlie Marsh
adad214619 Handle submodule imports for F401 (#58) 2022-08-31 11:24:25 -04:00
Charlie Marsh
0ebed13e67 Sort messages prior to display (#56) 2022-08-31 10:53:35 -04:00
Charlie Marsh
3afedcd48b Upgrade parser to handle more F821 cases (#57) 2022-08-31 10:52:54 -04:00
Charlie Marsh
875e812188 Disable build-on-push for now 2022-08-31 10:45:41 -04:00
Charlie Marsh
80f3cd0ef7 Don't assume errors appear in-order with line contents 2022-08-31 08:40:38 -04:00
Dmitry Dygalo
9e3c35e6dc Improve search for duplicates (#53) 2022-08-31 08:23:43 -04:00
Charlie Marsh
1e67ce229f Increment to v0.0.20 2022-08-30 14:41:28 -04:00
Charlie Marsh
643797d922 Support builtins (#50) 2022-08-30 14:41:17 -04:00
Charlie Marsh
e9a3484edf Implement F821 (#49) 2022-08-30 14:37:06 -04:00
Charlie Marsh
dd759e4730 Update graph 2022-08-30 13:41:30 -04:00
Adrian Garcia Badaracco
e16fd39bb5 Build on every push but only publish on tags (#46) 2022-08-30 13:19:36 -04:00
Charlie Marsh
7ed5b3d3a2 Avoid re-reading + iterating over lines for ignores (#48) 2022-08-30 13:19:22 -04:00
Charlie Marsh
ae27793b86 Remove extraneous Dockerfile 2022-08-30 09:24:57 -04:00
Charlie Marsh
641ff8452e Tweak README 2022-08-30 09:23:15 -04:00
Charlie Marsh
53554a2bf1 Remove lint step from release.yaml 2022-08-30 09:16:35 -04:00
Adrian Garcia Badaracco
bd0ed1b96c Build ABI3 wheels and expand supported platforms (#45) 2022-08-30 09:16:07 -04:00
Charlie Marsh
0cbcb982eb Implement F823 (#44) 2022-08-29 23:04:44 -04:00
Charlie Marsh
6c5845922f Remove caveat from README 2022-08-29 22:09:22 -04:00
40 changed files with 3571 additions and 691 deletions

View File

@@ -2,16 +2,16 @@ name: CI
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]
jobs:
cargo_build:
name: "cargo build"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -33,7 +33,7 @@ jobs:
name: "cargo fmt"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -55,7 +55,7 @@ jobs:
name: "cargo clippy"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -77,7 +77,7 @@ jobs:
name: "cargo test"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -99,13 +99,13 @@ jobs:
name: "maturin build"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: "3.10"
- run: pip install maturin
- uses: actions/cache@v3
env:

View File

@@ -5,36 +5,46 @@ on:
tags:
- v*
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PACKAGE_NAME: ruff
PYTHON_VERSION: "3.7" # to build abi3 wheels
jobs:
macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: aarch64-apple-darwin
profile: minimal
default: true
- name: Build wheels - x86_64
uses: messense/maturin-action@v1
with:
target: x86_64
args: -i python --release --out dist --sdist
args: --release --out dist --sdist
maturin-version: "v0.13.0"
- name: Install built wheel - x86_64
run: |
pip install ruff --no-index --find-links dist --force-reinstall
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Build wheels - universal2
uses: messense/maturin-action@v1
with:
args: -i python --release --universal2 --out dist
args: --release --universal2 --out dist
maturin-version: "v0.13.0"
- name: Install built wheel - universal2
run: |
pip install ruff --no-index --find-links dist --force-reinstall
pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
with:
@@ -47,13 +57,11 @@ jobs:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.target }}
- name: Update rustup
run: rustup self update
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
@@ -64,10 +72,12 @@ jobs:
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
args: -i python --release --out dist
args: --release --out dist
maturin-version: "v0.13.0"
- name: Install built wheel
shell: bash
run: |
pip install ruff --no-index --find-links dist --force-reinstall
python -m pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
with:
@@ -80,87 +90,192 @@ jobs:
matrix:
target: [x86_64, i686]
steps:
- uses: actions/checkout@v2
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
default: true
- uses: actions/setup-python@v2
with:
python-version: "3.10"
- name: Build Wheels
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: -i 3.10 --release --out dist
- name: Install built wheel
if: matrix.target == 'x86_64'
run: |
pip install ruff --no-index --find-links dist --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
with:
name: wheels
path: dist
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: Build wheels
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist
maturin-version: "v0.13.0"
- name: Install built wheel
if: matrix.target == 'x86_64'
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
with:
name: wheels
path: dist
linux-cross:
runs-on: ubuntu-latest
strategy:
matrix:
target: [aarch64, armv7, s390x, ppc64le]
target: [aarch64, armv7, s390x, ppc64le, ppc64]
steps:
- uses: actions/checkout@v2
- name: Build Wheels
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: -i 3.10 --release --out dist
- uses: uraimo/run-on-arch-action@v2.2.0
name: Install built wheel
with:
arch: ${{ matrix.target }}
distro: ubuntu20.04
githubToken: ${{ github.token }}
dockerRunArgs: |
--volume "${PWD}/dist:/artifacts"
install: |
apt-get update
apt-get install -y --no-install-recommends python3 python3-venv software-properties-common
add-apt-repository ppa:deadsnakes/ppa
apt-get update
apt-get install -y curl python3.7-venv python3.9-venv python3.10-venv
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Build wheels
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist
maturin-version: "v0.13.0"
- uses: uraimo/run-on-arch-action@v2.0.5
if: matrix.target != 'ppc64'
name: Install built wheel
with:
arch: ${{ matrix.target }}
distro: ubuntu20.04
githubToken: ${{ github.token }}
install: |
apt-get update
apt-get install -y --no-install-recommends python3 python3-pip
pip3 install -U pip
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
with:
name: wheels
path: dist
musllinux:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- x86_64-unknown-linux-musl
- i686-unknown-linux-musl
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: Build wheels
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
args: --release --out dist
maturin-version: "v0.13.0"
- name: Install built wheel
if: matrix.target == 'x86_64-unknown-linux-musl'
uses: addnab/docker-run-action@v3
with:
image: alpine:latest
options: -v ${{ github.workspace }}:/io -w /io
run: |
apk add py3-pip
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links /io/dist/ --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
with:
name: wheels
path: dist
musllinux-cross:
runs-on: ubuntu-latest
strategy:
matrix:
platform:
- target: aarch64-unknown-linux-musl
arch: aarch64
- target: armv7-unknown-linux-musleabihf
arch: armv7
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Build wheels
uses: messense/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2
args: --release --out dist
maturin-version: "v0.13.0"
- uses: uraimo/run-on-arch-action@master
name: Install built wheel
with:
arch: ${{ matrix.platform.arch }}
distro: alpine_latest
githubToken: ${{ github.token }}
install: |
apk add py3-pip
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
with:
name: wheels
path: dist
pypy:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
target: [x86_64, aarch64]
python-version:
- "3.7"
- "3.8"
- "3.9"
exclude:
- os: macos-latest
target: aarch64
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: pypy${{ matrix.python-version }}
- name: Build wheels
uses: messense/maturin-action@v1
with:
maturin-version: "v0.13.0"
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist -i pypy${{ matrix.python-version }}
- name: Install built wheel
if: matrix.target == 'x86_64'
run: |
ls -lrth /artifacts
PYTHON=python3.10
$PYTHON -m venv venv
venv/bin/pip install -U pip
venv/bin/pip install ruff --no-index --find-links /artifacts --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
with:
name: wheels
path: dist
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v2
with:
name: wheels
path: dist
release:
name: Release
runs-on: ubuntu-latest
needs:
- macos
- windows
- linux
- linux-cross
- musllinux
- musllinux-cross
- pypy
if: "startsWith(github.ref, 'refs/tags/')"
needs: [ macos, windows, linux, linux-cross ]
steps:
- uses: actions/download-artifact@v2
with:
name: wheels
- uses: actions/setup-python@v2
with:
python-version: "3.10"
- name: Publish to PyPI
- uses: actions/setup-python@v4
- name: Publish to PyPi
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
pip install --upgrade wheel pip setuptools twine
pip install --upgrade twine
twine upload --skip-existing *

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
# Local cache
.cache
.ruff_cache
resources/test/cpython
###

5
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,5 @@
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.24
hooks:
- id: lint

7
.pre-commit-hooks.yaml Normal file
View File

@@ -0,0 +1,7 @@
- id: lint
name: ruff lint
description: Run ruff to lint Python files.
entry: ruff
language: python
types_or: [python]
pass_filenames: true

930
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,13 @@
[package]
name = "ruff"
version = "0.0.19"
version = "0.0.24"
edition = "2021"
[lib]
name = "ruff"
[dependencies]
libcst = { path = "../LibCST/native/libcst" }
anyhow = { version = "1.0.60" }
bincode = { version = "1.3.3" }
cacache = { version = "10.0.1" }
@@ -18,16 +19,22 @@ common-path = { version = "1.0.0" }
dirs = { version = "4.0.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.17" }
glob = { version = "0.3.0"}
log = { version = "0.4.17" }
notify = { version = "4.0.17" }
once_cell = { version = "1.13.1" }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "7a688e0b6c2904f286acac1e4af3f1628dd38589" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "1613f6c6990011a4bc559e79aaf28d715f9f729b" }
serde = { version = "1.0.143", features = ["derive"] }
serde_json = { version = "1.0.83" }
toml = { version = "0.5.9" }
walkdir = { version = "2.3.2" }
bumpalo = "3.11.0"
bat = "0.21.0"
[profile.release]
lto = true
panic = "abort"
lto = "thin"
codegen-units = 1
opt-level = 3

View File

@@ -6,25 +6,25 @@
An extremely fast Python linter, written in Rust.
<p align="center">
<img alt="Bar chart with benchmark results" src="https://user-images.githubusercontent.com/1309177/187330134-ac05076c-8d16-4451-a300-986692b34abf.svg">
<img alt="Bar chart with benchmark results" src="https://user-images.githubusercontent.com/1309177/187504482-6d9df992-a81d-4e86-9f6a-d958741c8182.svg">
</p>
<p align="center">
<i>Linting the CPython codebase from scratch.</i>
</p>
Major features:
- 10-100x faster than existing linters.
- Installable via `pip`.
- Python 3.10 compatibility.
- [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired cache semantics.
- [TypeScript](https://www.typescriptlang.org/docs/handbook/configuring-watch.html)-inspired `--watch` semantics.
- `pyproject.toml` support.
- ⚡️ 10-100x faster than existing linters
- 🐍 Installable via `pip`
- 🤝 Python 3.10 compatibility
- 🛠️ `pyproject.toml` support
- 📦 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired cache semantics
- 👀 [TypeScript](https://www.typescriptlang.org/docs/handbook/configuring-watch.html)-inspired `--watch` semantics
_ruff is a proof-of-concept and not yet intended for production use. It supports only a small subset
of the Flake8 rules, and may crash on your codebase._
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
## Installation and usage
### Installation
@@ -35,11 +35,6 @@ Available as [ruff](https://pypi.org/project/ruff/) on PyPI:
pip install ruff
```
For now, wheels are available for Python 3.7, 3.8, 3.9, and 3.10 on macOS, Windows, and Linux. If a
wheel isn't available for your Python version or platform, you'll need to install the [Rust toolchain](https://www.rust-lang.org/tools/install)
prior to running `pip install ruff`. (This is an effort limitation on my part, not a technical
limitation.)
### Usage
To run ruff, try any of the following:
@@ -56,6 +51,16 @@ You can run ruff in `--watch` mode to automatically re-run on-change:
ruff path/to/code/ --watch
```
ruff also works with [Pre-Commit](https://pre-commit.com) (requires Cargo on system):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.24
hooks:
- id: lint
```
## Configuration
ruff is configurable both via `pyproject.toml` and the command line.
@@ -81,7 +86,7 @@ See `ruff --help` for more:
```shell
ruff
A Python linter written in Rust
An extremely fast Python linter.
USAGE:
ruff [OPTIONS] <FILES>...
@@ -220,6 +225,7 @@ hyperfine --ignore-failure --warmup 5 \
```
In order, these evaluate:
- ruff
- Pylint
- PyFlakes

View File

@@ -1,5 +0,0 @@
FROM python:3.10.6-buster
RUN pip install ruff
RUN touch foo.py
RUN ruff foo.py

10
remove_object_base.py Normal file
View File

@@ -0,0 +1,10 @@
class Foo(object):
pass
class Bar(Foo, object):
pass
class Baz(Foo, Bar, object):
pass

View File

@@ -0,0 +1 @@
bad: str = f"bad" + "bad"

View File

@@ -1,3 +1,4 @@
from __future__ import all_feature_names
import os
import functools
from collections import (
@@ -5,10 +6,20 @@ from collections import (
OrderedDict,
namedtuple,
)
import multiprocessing.pool
import multiprocessing.process
import logging.config
import logging.handlers
from blah import ClassA, ClassB, ClassC
class X:
def a(self) -> "namedtuple":
x = os.environ["1"]
y = Counter()
return X
z = multiprocessing.pool.ThreadPool()
__all__ = ["ClassA"] + ["ClassB"]
__all__ += ["ClassC"]

View File

@@ -6,3 +6,7 @@ for _ in range(5):
pass
elif (3, 4):
pass
class Foo(object):
pass

View File

@@ -0,0 +1,10 @@
def f() -> int:
yield 1
class Foo:
yield 2
yield 3
yield from 3

View File

@@ -0,0 +1,46 @@
def get_name():
return self.name
def get_name():
return (self.name,)
def get_name():
del self.name
def get_name(self):
return self.name
x = list()
def randdec(maxprec, maxexp):
return numeric_string(maxprec, maxexp)
def ternary_optarg(prec, exp_range, itr):
for _ in range(100):
a = randdec(prec, 2 * exp_range)
b = randdec(prec, 2 * exp_range)
c = randdec(prec, 2 * exp_range)
yield a, b, c, None
yield a, b, c, None, None
class Foo:
CLASS_VAR = 1
REFERENCES_CLASS_VAR = {"CLASS_VAR": CLASS_VAR}
class Class:
def __init__(self):
# TODO(charlie): This should be recognized as a defined variable.
Class # noqa: F821
try:
x = 1 / 0
except Exception as e:
print(e)

View File

@@ -0,0 +1,27 @@
my_dict = {}
my_var = 0
def foo():
my_var += 1
def bar():
global my_var
my_var += 1
def baz():
global my_var
global my_dict
my_dict[my_var] += 1
def dec(x):
return x
@dec
def f():
dec = 1
return dec

View File

@@ -0,0 +1,16 @@
try:
1 / 0
except ValueError as e:
pass
try:
1 / 0
except ValueError as e:
print(e)
def f():
x = 1
y = 2
z = x + y

View File

View File

@@ -0,0 +1,9 @@
a = "abc"
b = f"ghi{'jkl'}"
c = f"def"
d = f"def" + "ghi"
e = (
f"def" +
"ghi"
)

View File

View File

@@ -0,0 +1,9 @@
a = "abc"
b = f"ghi{'jkl'}"
c = f"def"
d = f"def" + "ghi"
e = (
f"def" +
"ghi"
)

View File

@@ -1,13 +1,17 @@
[tool.ruff]
line-length = 88
exclude = ["excluded.py"]
exclude = ["excluded.py", "**/migrations"]
select = [
"E501",
"F401",
"F403",
"F541",
"F634",
"F704",
"F706",
"F821",
"F823",
"F831",
"F841",
"F901",
]

117
src/ast_ops.rs Normal file
View File

@@ -0,0 +1,117 @@
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
fn id() -> usize {
static COUNTER: AtomicUsize = AtomicUsize::new(1);
COUNTER.fetch_add(1, Ordering::Relaxed)
}
pub enum ScopeKind {
Class,
Function,
Generator,
Module,
}
pub struct Scope {
pub id: usize,
pub kind: ScopeKind,
pub values: BTreeMap<String, Binding>,
}
impl Scope {
pub fn new(kind: ScopeKind) -> Self {
Scope {
id: id(),
kind,
values: BTreeMap::new(),
}
}
}
#[derive(Clone, Debug)]
pub enum BindingKind {
Argument,
Assignment,
Builtin,
ClassDefinition,
Definition,
Export(Vec<String>),
FutureImportation,
Importation(String),
StarImportation,
SubmoduleImportation(String),
}
#[derive(Clone, Debug)]
pub struct Binding {
pub kind: BindingKind,
pub location: Location,
pub used: Option<usize>,
}
/// Extract the names bound to a given __all__ assignment.
pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
let mut names: Vec<String> = vec![];
fn add_to_names(names: &mut Vec<String>, elts: &[Expr]) {
for elt in elts {
if let ExprKind::Constant {
value: Constant::Str(value),
..
} = &elt.node
{
names.push(value.to_string())
}
}
}
// Grab the existing bound __all__ values.
if let StmtKind::AugAssign { .. } = &stmt.node {
if let Some(binding) = scope.values.get("__all__") {
if let BindingKind::Export(existing) = &binding.kind {
names.extend(existing.clone());
}
}
}
if let Some(value) = match &stmt.node {
StmtKind::Assign { value, .. } => Some(value),
StmtKind::AnnAssign { value, .. } => value.as_ref(),
StmtKind::AugAssign { value, .. } => Some(value),
_ => None,
} {
match &value.node {
ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => {
add_to_names(&mut names, elts)
}
ExprKind::BinOp { left, right, .. } => {
let mut current_left = left;
let mut current_right = right;
while let Some(elts) = match &current_right.node {
ExprKind::List { elts, .. } => Some(elts),
ExprKind::Tuple { elts, .. } => Some(elts),
_ => None,
} {
add_to_names(&mut names, elts);
match &current_left.node {
ExprKind::BinOp { left, right, .. } => {
current_left = left;
current_right = right;
}
ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => {
add_to_names(&mut names, elts);
break;
}
_ => break,
}
}
}
_ => {}
}
}
names
}

View File

@@ -4,19 +4,16 @@ use rustpython_parser::ast::{
PatternKind, Stmt, StmtKind, Unaryop, Withitem,
};
pub trait Visitor {
pub trait ASTVisitor {
fn visit_stmt(&mut self, stmt: &Stmt) {
walk_stmt(self, stmt);
}
fn visit_annotation(&mut self, expr: &Expr) {
walk_expr(self, expr);
}
fn visit_expr(&mut self, expr: &Expr) {
fn visit_expr(&mut self, expr: &Expr, _parent: Option<&Stmt>) {
walk_expr(self, expr);
}
fn visit_ident(&mut self, ident: &str) {
walk_ident(self, ident);
}
fn visit_constant(&mut self, constant: &Constant) {
walk_constant(self, constant);
}
@@ -64,89 +61,45 @@ pub trait Visitor {
}
}
pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
pub fn walk_stmt<V: ASTVisitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
match &stmt.node {
StmtKind::FunctionDef {
name,
args,
body,
decorator_list,
returns,
..
} => {
visitor.visit_ident(name);
StmtKind::FunctionDef { args, body, .. } => {
visitor.visit_arguments(args);
for stmt in body {
visitor.visit_stmt(stmt)
}
for expr in decorator_list {
visitor.visit_expr(expr)
}
for expr in returns {
visitor.visit_annotation(expr);
}
}
StmtKind::AsyncFunctionDef {
name,
args,
body,
decorator_list,
returns,
..
} => {
visitor.visit_ident(name);
StmtKind::AsyncFunctionDef { args, body, .. } => {
visitor.visit_arguments(args);
for stmt in body {
visitor.visit_stmt(stmt)
}
for expr in decorator_list {
visitor.visit_expr(expr)
}
for expr in returns {
visitor.visit_annotation(expr);
}
}
StmtKind::ClassDef {
name,
bases,
keywords,
body,
decorator_list,
} => {
visitor.visit_ident(name);
for expr in bases {
visitor.visit_expr(expr)
}
for keyword in keywords {
visitor.visit_keyword(keyword)
}
StmtKind::ClassDef { body, .. } => {
for stmt in body {
visitor.visit_stmt(stmt)
}
for expr in decorator_list {
visitor.visit_expr(expr)
}
}
StmtKind::Return { value } => {
if let Some(expr) = value {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
}
StmtKind::Delete { targets } => {
for expr in targets {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
}
StmtKind::Assign { targets, value, .. } => {
for expr in targets {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
visitor.visit_expr(value)
visitor.visit_expr(value, Some(stmt))
}
StmtKind::AugAssign { target, op, value } => {
visitor.visit_expr(target);
visitor.visit_expr(target, Some(stmt));
visitor.visit_operator(op);
visitor.visit_expr(value);
visitor.visit_expr(value, Some(stmt));
}
StmtKind::AnnAssign {
target,
@@ -154,10 +107,10 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
value,
..
} => {
visitor.visit_expr(target);
visitor.visit_expr(target, Some(stmt));
visitor.visit_annotation(annotation);
if let Some(expr) = value {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
}
StmtKind::For {
@@ -167,8 +120,8 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
orelse,
..
} => {
visitor.visit_expr(target);
visitor.visit_expr(iter);
visitor.visit_expr(target, Some(stmt));
visitor.visit_expr(iter, Some(stmt));
for stmt in body {
visitor.visit_stmt(stmt)
}
@@ -183,8 +136,8 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
orelse,
..
} => {
visitor.visit_expr(target);
visitor.visit_expr(iter);
visitor.visit_expr(target, Some(stmt));
visitor.visit_expr(iter, Some(stmt));
for stmt in body {
visitor.visit_stmt(stmt)
}
@@ -193,7 +146,7 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
}
StmtKind::While { test, body, orelse } => {
visitor.visit_expr(test);
visitor.visit_expr(test, Some(stmt));
for stmt in body {
visitor.visit_stmt(stmt)
}
@@ -202,7 +155,7 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
}
StmtKind::If { test, body, orelse } => {
visitor.visit_expr(test);
visitor.visit_expr(test, Some(stmt));
for stmt in body {
visitor.visit_stmt(stmt)
}
@@ -228,17 +181,17 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
StmtKind::Match { subject, cases } => {
// TODO(charlie): Handle `cases`.
visitor.visit_expr(subject);
visitor.visit_expr(subject, Some(stmt));
for match_case in cases {
visitor.visit_match_case(match_case);
}
}
StmtKind::Raise { exc, cause } => {
if let Some(expr) = exc {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
};
if let Some(expr) = cause {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
};
}
StmtKind::Try {
@@ -261,9 +214,9 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
}
}
StmtKind::Assert { test, msg } => {
visitor.visit_expr(test);
visitor.visit_expr(test, None);
if let Some(expr) = msg {
visitor.visit_expr(expr)
visitor.visit_expr(expr, Some(stmt))
}
}
StmtKind::Import { names } => {
@@ -271,121 +224,110 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
visitor.visit_alias(alias);
}
}
StmtKind::ImportFrom { module, names, .. } => {
if let Some(ident) = module {
visitor.visit_ident(ident);
}
StmtKind::ImportFrom { names, .. } => {
for alias in names {
visitor.visit_alias(alias);
}
}
StmtKind::Global { names } => {
for ident in names {
visitor.visit_ident(ident)
}
}
StmtKind::Nonlocal { names } => {
for ident in names {
visitor.visit_ident(ident)
}
}
StmtKind::Expr { value } => visitor.visit_expr(value),
StmtKind::Global { .. } => {}
StmtKind::Nonlocal { .. } => {}
StmtKind::Expr { value } => visitor.visit_expr(value, Some(stmt)),
StmtKind::Pass => {}
StmtKind::Break => {}
StmtKind::Continue => {}
}
}
pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
pub fn walk_expr<V: ASTVisitor + ?Sized>(visitor: &mut V, expr: &Expr) {
match &expr.node {
ExprKind::BoolOp { op, values } => {
visitor.visit_boolop(op);
for expr in values {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::NamedExpr { target, value } => {
visitor.visit_expr(target);
visitor.visit_expr(value);
visitor.visit_expr(target, None);
visitor.visit_expr(value, None);
}
ExprKind::BinOp { left, op, right } => {
visitor.visit_expr(left);
visitor.visit_expr(left, None);
visitor.visit_operator(op);
visitor.visit_expr(right);
visitor.visit_expr(right, None);
}
ExprKind::UnaryOp { op, operand } => {
visitor.visit_unaryop(op);
visitor.visit_expr(operand);
visitor.visit_expr(operand, None);
}
ExprKind::Lambda { args, body } => {
visitor.visit_arguments(args);
visitor.visit_expr(body);
visitor.visit_expr(body, None);
}
ExprKind::IfExp { test, body, orelse } => {
visitor.visit_expr(test);
visitor.visit_expr(body);
visitor.visit_expr(orelse);
visitor.visit_expr(test, None);
visitor.visit_expr(body, None);
visitor.visit_expr(orelse, None);
}
ExprKind::Dict { keys, values } => {
for expr in keys {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
for expr in values {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::Set { elts } => {
for expr in elts {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::ListComp { elt, generators } => {
visitor.visit_expr(elt);
for comprehension in generators {
visitor.visit_comprehension(comprehension)
}
visitor.visit_expr(elt, None);
}
ExprKind::SetComp { elt, generators } => {
visitor.visit_expr(elt);
for comprehension in generators {
visitor.visit_comprehension(comprehension)
}
visitor.visit_expr(elt, None);
}
ExprKind::DictComp {
key,
value,
generators,
} => {
visitor.visit_expr(key);
visitor.visit_expr(value);
for comprehension in generators {
visitor.visit_comprehension(comprehension)
}
visitor.visit_expr(key, None);
visitor.visit_expr(value, None);
}
ExprKind::GeneratorExp { elt, generators } => {
visitor.visit_expr(elt);
for comprehension in generators {
visitor.visit_comprehension(comprehension)
}
visitor.visit_expr(elt, None);
}
ExprKind::Await { value } => visitor.visit_expr(value),
ExprKind::Await { value } => visitor.visit_expr(value, None),
ExprKind::Yield { value } => {
if let Some(expr) = value {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::YieldFrom { value } => visitor.visit_expr(value),
ExprKind::YieldFrom { value } => visitor.visit_expr(value, None),
ExprKind::Compare {
left,
ops,
comparators,
} => {
visitor.visit_expr(left);
visitor.visit_expr(left, None);
for cmpop in ops {
visitor.visit_cmpop(cmpop);
}
for expr in comparators {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::Call {
@@ -393,9 +335,9 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
args,
keywords,
} => {
visitor.visit_expr(func);
visitor.visit_expr(func, None);
for expr in args {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
for keyword in keywords {
visitor.visit_keyword(keyword);
@@ -404,61 +346,60 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
ExprKind::FormattedValue {
value, format_spec, ..
} => {
visitor.visit_expr(value);
visitor.visit_expr(value, None);
if let Some(expr) = format_spec {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::JoinedStr { values } => {
for expr in values {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
ExprKind::Constant { value, .. } => visitor.visit_constant(value),
ExprKind::Attribute { value, ctx, .. } => {
visitor.visit_expr(value);
visitor.visit_expr(value, None);
visitor.visit_expr_context(ctx);
}
ExprKind::Subscript { value, slice, ctx } => {
visitor.visit_expr(value);
visitor.visit_expr(slice);
visitor.visit_expr(value, None);
visitor.visit_expr(slice, None);
visitor.visit_expr_context(ctx);
}
ExprKind::Starred { value, ctx } => {
visitor.visit_expr(value);
visitor.visit_expr(value, None);
visitor.visit_expr_context(ctx);
}
ExprKind::Name { id, ctx } => {
visitor.visit_ident(id);
ExprKind::Name { ctx, .. } => {
visitor.visit_expr_context(ctx);
}
ExprKind::List { elts, ctx } => {
for expr in elts {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
visitor.visit_expr_context(ctx);
}
ExprKind::Tuple { elts, ctx } => {
for expr in elts {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
visitor.visit_expr_context(ctx);
}
ExprKind::Slice { lower, upper, step } => {
if let Some(expr) = lower {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
if let Some(expr) = upper {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
if let Some(expr) = step {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
}
}
}
pub fn walk_constant<V: Visitor + ?Sized>(visitor: &mut V, constant: &Constant) {
pub fn walk_constant<V: ASTVisitor + ?Sized>(visitor: &mut V, constant: &Constant) {
if let Constant::Tuple(constants) = constant {
for constant in constants {
visitor.visit_constant(constant)
@@ -466,22 +407,19 @@ pub fn walk_constant<V: Visitor + ?Sized>(visitor: &mut V, constant: &Constant)
}
}
pub fn walk_comprehension<V: Visitor + ?Sized>(visitor: &mut V, comprehension: &Comprehension) {
visitor.visit_expr(&comprehension.target);
visitor.visit_expr(&comprehension.iter);
pub fn walk_comprehension<V: ASTVisitor + ?Sized>(visitor: &mut V, comprehension: &Comprehension) {
visitor.visit_expr(&comprehension.target, None);
visitor.visit_expr(&comprehension.iter, None);
for expr in &comprehension.ifs {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
}
pub fn walk_excepthandler<V: Visitor + ?Sized>(visitor: &mut V, excepthandler: &Excepthandler) {
pub fn walk_excepthandler<V: ASTVisitor + ?Sized>(visitor: &mut V, excepthandler: &Excepthandler) {
match &excepthandler.node {
ExcepthandlerKind::ExceptHandler { type_, name, body } => {
ExcepthandlerKind::ExceptHandler { type_, body, .. } => {
if let Some(expr) = type_ {
visitor.visit_expr(expr);
}
if let Some(ident) = name {
visitor.visit_ident(ident);
visitor.visit_expr(expr, None);
}
for stmt in body {
visitor.visit_stmt(stmt);
@@ -490,7 +428,7 @@ pub fn walk_excepthandler<V: Visitor + ?Sized>(visitor: &mut V, excepthandler: &
}
}
pub fn walk_arguments<V: Visitor + ?Sized>(visitor: &mut V, arguments: &Arguments) {
pub fn walk_arguments<V: ASTVisitor + ?Sized>(visitor: &mut V, arguments: &Arguments) {
for arg in &arguments.posonlyargs {
visitor.visit_arg(arg);
}
@@ -504,96 +442,80 @@ pub fn walk_arguments<V: Visitor + ?Sized>(visitor: &mut V, arguments: &Argument
visitor.visit_arg(arg);
}
for expr in &arguments.kw_defaults {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
if let Some(arg) = &arguments.kwarg {
visitor.visit_arg(arg)
}
for expr in &arguments.defaults {
visitor.visit_expr(expr)
visitor.visit_expr(expr, None)
}
}
pub fn walk_arg<V: Visitor + ?Sized>(visitor: &mut V, arg: &Arg) {
pub fn walk_arg<V: ASTVisitor + ?Sized>(visitor: &mut V, arg: &Arg) {
if let Some(expr) = &arg.node.annotation {
visitor.visit_annotation(expr)
}
}
pub fn walk_keyword<V: Visitor + ?Sized>(visitor: &mut V, keyword: &Keyword) {
visitor.visit_expr(&keyword.node.value);
pub fn walk_keyword<V: ASTVisitor + ?Sized>(visitor: &mut V, keyword: &Keyword) {
visitor.visit_expr(&keyword.node.value, None);
}
pub fn walk_withitem<V: Visitor + ?Sized>(visitor: &mut V, withitem: &Withitem) {
visitor.visit_expr(&withitem.context_expr);
pub fn walk_withitem<V: ASTVisitor + ?Sized>(visitor: &mut V, withitem: &Withitem) {
visitor.visit_expr(&withitem.context_expr, None);
if let Some(expr) = &withitem.optional_vars {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
}
pub fn walk_match_case<V: Visitor + ?Sized>(visitor: &mut V, match_case: &MatchCase) {
pub fn walk_match_case<V: ASTVisitor + ?Sized>(visitor: &mut V, match_case: &MatchCase) {
visitor.visit_pattern(&match_case.pattern);
if let Some(expr) = &match_case.guard {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
for stmt in &match_case.body {
visitor.visit_stmt(stmt);
}
}
pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
pub fn walk_pattern<V: ASTVisitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
match &pattern.node {
PatternKind::MatchValue { value } => visitor.visit_expr(value),
PatternKind::MatchValue { value } => visitor.visit_expr(value, None),
PatternKind::MatchSingleton { value } => visitor.visit_constant(value),
PatternKind::MatchSequence { patterns } => {
for pattern in patterns {
visitor.visit_pattern(pattern)
}
}
PatternKind::MatchMapping {
keys,
patterns,
rest,
} => {
PatternKind::MatchMapping { keys, patterns, .. } => {
for expr in keys {
visitor.visit_expr(expr);
visitor.visit_expr(expr, None);
}
for pattern in patterns {
visitor.visit_pattern(pattern);
}
if let Some(ident) = rest {
visitor.visit_ident(ident);
}
}
PatternKind::MatchClass {
cls,
patterns,
kwd_attrs,
kwd_patterns,
..
} => {
visitor.visit_expr(cls);
visitor.visit_expr(cls, None);
for pattern in patterns {
visitor.visit_pattern(pattern);
}
for ident in kwd_attrs {
visitor.visit_ident(ident);
}
for pattern in kwd_patterns {
visitor.visit_pattern(pattern);
}
}
PatternKind::MatchStar { name } => {
if let Some(ident) = name {
visitor.visit_ident(ident)
}
}
PatternKind::MatchAs { pattern, name } => {
PatternKind::MatchStar { .. } => {}
PatternKind::MatchAs { pattern, .. } => {
if let Some(pattern) = pattern {
visitor.visit_pattern(pattern)
}
if let Some(ident) = name {
visitor.visit_ident(ident)
}
}
PatternKind::MatchOr { patterns } => {
for pattern in patterns {
@@ -604,22 +526,25 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
}
#[allow(unused_variables)]
pub fn walk_ident<V: Visitor + ?Sized>(visitor: &mut V, ident: &str) {}
#[inline(always)]
pub fn walk_expr_context<V: ASTVisitor + ?Sized>(visitor: &mut V, expr_context: &ExprContext) {}
#[allow(unused_variables)]
pub fn walk_expr_context<V: Visitor + ?Sized>(visitor: &mut V, expr_context: &ExprContext) {}
#[inline(always)]
pub fn walk_boolop<V: ASTVisitor + ?Sized>(visitor: &mut V, boolop: &Boolop) {}
#[allow(unused_variables)]
pub fn walk_boolop<V: Visitor + ?Sized>(visitor: &mut V, boolop: &Boolop) {}
#[inline(always)]
pub fn walk_operator<V: ASTVisitor + ?Sized>(visitor: &mut V, operator: &Operator) {}
#[allow(unused_variables)]
pub fn walk_operator<V: Visitor + ?Sized>(visitor: &mut V, operator: &Operator) {}
#[inline(always)]
pub fn walk_unaryop<V: ASTVisitor + ?Sized>(visitor: &mut V, unaryop: &Unaryop) {}
#[allow(unused_variables)]
pub fn walk_unaryop<V: Visitor + ?Sized>(visitor: &mut V, unaryop: &Unaryop) {}
#[inline(always)]
pub fn walk_cmpop<V: ASTVisitor + ?Sized>(visitor: &mut V, cmpop: &Cmpop) {}
#[allow(unused_variables)]
pub fn walk_cmpop<V: Visitor + ?Sized>(visitor: &mut V, cmpop: &Cmpop) {}
#[allow(unused_variables)]
pub fn walk_alias<V: Visitor + ?Sized>(visitor: &mut V, alias: &Alias) {}
#[inline(always)]
pub fn walk_alias<V: ASTVisitor + ?Sized>(visitor: &mut V, alias: &Alias) {}

163
src/builtins.rs Normal file
View File

@@ -0,0 +1,163 @@
pub const BUILTINS: &[&str] = &[
"ArithmeticError",
"AssertionError",
"AttributeError",
"BaseException",
"BlockingIOError",
"BrokenPipeError",
"BufferError",
"BytesWarning",
"ChildProcessError",
"ConnectionAbortedError",
"ConnectionError",
"ConnectionRefusedError",
"ConnectionResetError",
"DeprecationWarning",
"EOFError",
"Ellipsis",
"EnvironmentError",
"Exception",
"False",
"FileExistsError",
"FileNotFoundError",
"FloatingPointError",
"FutureWarning",
"GeneratorExit",
"IOError",
"ImportError",
"ImportWarning",
"IndentationError",
"IndexError",
"InterruptedError",
"IsADirectoryError",
"KeyError",
"KeyboardInterrupt",
"LookupError",
"MemoryError",
"ModuleNotFoundError",
"NameError",
"None",
"NotADirectoryError",
"NotImplemented",
"NotImplementedError",
"OSError",
"OverflowError",
"PendingDeprecationWarning",
"PermissionError",
"ProcessLookupError",
"RecursionError",
"ReferenceError",
"ResourceWarning",
"RuntimeError",
"RuntimeWarning",
"StopAsyncIteration",
"StopIteration",
"SyntaxError",
"SyntaxWarning",
"SystemError",
"SystemExit",
"TabError",
"TimeoutError",
"True",
"TypeError",
"UnboundLocalError",
"UnicodeDecodeError",
"UnicodeEncodeError",
"UnicodeError",
"UnicodeTranslateError",
"UnicodeWarning",
"UserWarning",
"ValueError",
"Warning",
"ZeroDivisionError",
"__build_class__",
"__debug__",
"__doc__",
"__import__",
"__loader__",
"__name__",
"__package__",
"__spec__",
"abs",
"all",
"any",
"ascii",
"bin",
"bool",
"breakpoint",
"bytearray",
"bytes",
"callable",
"chr",
"classmethod",
"compile",
"complex",
"copyright",
"credits",
"delattr",
"dict",
"dir",
"divmod",
"enumerate",
"eval",
"exec",
"exit",
"filter",
"float",
"format",
"frozenset",
"getattr",
"globals",
"hasattr",
"hash",
"help",
"hex",
"id",
"input",
"int",
"isinstance",
"issubclass",
"iter",
"len",
"license",
"list",
"locals",
"map",
"max",
"memoryview",
"min",
"next",
"object",
"oct",
"open",
"ord",
"pow",
"print",
"property",
"quit",
"range",
"repr",
"reversed",
"round",
"set",
"setattr",
"slice",
"sorted",
"staticmethod",
"str",
"sum",
"super",
"tuple",
"type",
"vars",
"zip",
];
// Globally defined names which are not attributes of the builtins module, or are only present on
// some platforms.
pub const MAGIC_GLOBALS: &[&str] = &[
"__file__",
"__builtins__",
"__annotations__",
"WindowsError",
];

View File

@@ -66,7 +66,7 @@ impl From<bool> for Mode {
}
fn cache_dir() -> &'static str {
"./.cache"
"./.ruff_cache"
}
fn cache_key(path: &Path, settings: &Settings) -> String {

View File

@@ -1,45 +1,17 @@
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeSet;
use rustpython_parser::ast::{
Arg, Arguments, Constant, Expr, ExprContext, ExprKind, Location, Stmt, StmtKind, Suite,
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Stmt,
StmtKind, Suite,
};
use rustpython_parser::parser;
use crate::check_ast::ScopeKind::{Class, Function, Generator, Module};
use crate::ast_ops::{extract_all_names, Binding, BindingKind, Scope, ScopeKind};
use crate::ast_visitor;
use crate::ast_visitor::{walk_excepthandler, ASTVisitor};
use crate::builtins::{BUILTINS, MAGIC_GLOBALS};
use crate::checks::{Check, CheckCode, CheckKind};
use crate::settings::Settings;
use crate::visitor;
use crate::visitor::Visitor;
enum ScopeKind {
Class,
Function,
Generator,
Module,
}
struct Scope {
kind: ScopeKind,
values: BTreeMap<String, Binding>,
}
enum BindingKind {
Argument,
Assignment,
ClassDefinition,
Definition,
FutureImportation,
Importation(String),
StarImportation,
SubmoduleImportation,
}
struct Binding {
kind: BindingKind,
name: String,
location: Location,
used: bool,
}
struct Checker<'a> {
settings: &'a Settings,
@@ -65,32 +37,56 @@ impl Checker<'_> {
}
}
impl Visitor for Checker<'_> {
impl ASTVisitor for Checker<'_> {
fn visit_stmt(&mut self, stmt: &Stmt) {
match &stmt.node {
StmtKind::FunctionDef { name, .. } => {
self.push_scope(Scope {
kind: Function,
values: BTreeMap::new(),
});
self.add_binding(Binding {
kind: BindingKind::ClassDefinition,
name: name.clone(),
used: false,
location: stmt.location,
})
StmtKind::Global { names } | StmtKind::Nonlocal { names } => {
// TODO(charlie): Handle doctests.
let global_scope_index = 0;
let global_scope_id = self.scopes[global_scope_index].id;
let current_scope_id = self.scopes.last().expect("No current scope found.").id;
if current_scope_id != global_scope_id {
for name in names {
for scope in self.scopes.iter_mut().skip(global_scope_index + 1) {
scope.values.insert(
name.to_string(),
Binding {
kind: BindingKind::Assignment,
used: Some(global_scope_id),
location: stmt.location,
},
);
}
}
}
}
StmtKind::AsyncFunctionDef { name, .. } => {
self.push_scope(Scope {
kind: Function,
values: BTreeMap::new(),
});
self.add_binding(Binding {
kind: BindingKind::ClassDefinition,
name: name.clone(),
used: false,
location: stmt.location,
})
StmtKind::FunctionDef {
name,
decorator_list,
returns,
..
}
| StmtKind::AsyncFunctionDef {
name,
decorator_list,
returns,
..
} => {
for expr in decorator_list {
self.visit_expr(expr, Some(stmt));
}
for expr in returns {
self.visit_annotation(expr);
}
self.add_binding(
name.to_string(),
Binding {
kind: BindingKind::Definition,
used: None,
location: stmt.location,
},
);
self.push_scope(Scope::new(ScopeKind::Function));
}
StmtKind::Return { .. } => {
if self
@@ -100,7 +96,7 @@ impl Visitor for Checker<'_> {
{
if let Some(scope) = self.scopes.last() {
match scope.kind {
Class | Module => {
ScopeKind::Class | ScopeKind::Module => {
self.checks.push(Check {
kind: CheckKind::ReturnOutsideFunction,
location: stmt.location,
@@ -111,36 +107,57 @@ impl Visitor for Checker<'_> {
}
}
}
StmtKind::ClassDef { .. } => self.push_scope(Scope {
kind: Class,
values: BTreeMap::new(),
}),
StmtKind::ClassDef {
bases,
keywords,
decorator_list,
..
} => {
for expr in bases {
self.visit_expr(expr, Some(stmt))
}
for keyword in keywords {
self.visit_keyword(keyword)
}
for expr in decorator_list {
self.visit_expr(expr, Some(stmt))
}
self.push_scope(Scope::new(ScopeKind::Class))
}
StmtKind::Import { names } => {
for alias in names {
if alias.node.name.contains('.') && alias.node.asname.is_none() {
self.add_binding(Binding {
kind: BindingKind::SubmoduleImportation,
name: alias.node.name.clone(),
used: false,
location: stmt.location,
})
// TODO(charlie): Multiple submodule imports with the same parent module
// will be merged into a single binding.
self.add_binding(
alias.node.name.split('.').next().unwrap().to_string(),
Binding {
kind: BindingKind::SubmoduleImportation(
alias.node.name.to_string(),
),
used: None,
location: stmt.location,
},
)
} else {
self.add_binding(Binding {
kind: BindingKind::Importation(
alias
.node
.asname
.clone()
.unwrap_or_else(|| alias.node.name.clone()),
),
name: alias
self.add_binding(
alias
.node
.asname
.clone()
.unwrap_or_else(|| alias.node.name.clone()),
used: false,
location: stmt.location,
})
Binding {
kind: BindingKind::Importation(
alias
.node
.asname
.clone()
.unwrap_or_else(|| alias.node.name.clone()),
),
used: None,
location: stmt.location,
},
)
}
}
}
@@ -151,20 +168,24 @@ impl Visitor for Checker<'_> {
.asname
.clone()
.unwrap_or_else(|| alias.node.name.clone());
if let Some("future") = module.as_deref() {
self.add_binding(Binding {
kind: BindingKind::FutureImportation,
if let Some("__future__") = module.as_deref() {
self.add_binding(
name,
used: true,
location: stmt.location,
});
Binding {
kind: BindingKind::FutureImportation,
used: Some(self.scopes.last().expect("No current scope found.").id),
location: stmt.location,
},
);
} else if alias.node.name == "*" {
self.add_binding(Binding {
kind: BindingKind::StarImportation,
self.add_binding(
name,
used: false,
location: stmt.location,
});
Binding {
kind: BindingKind::StarImportation,
used: None,
location: stmt.location,
},
);
if self
.settings
@@ -177,15 +198,15 @@ impl Visitor for Checker<'_> {
});
}
} else {
self.add_binding(Binding {
let binding = Binding {
kind: BindingKind::Importation(match module {
None => name.clone(),
Some(parent) => format!("{}.{}", parent, name),
Some(parent) => format!("{}.{}", parent, name.clone()),
}),
name,
used: false,
used: None,
location: stmt.location,
})
};
self.add_binding(name, binding)
}
}
}
@@ -230,56 +251,90 @@ impl Visitor for Checker<'_> {
}
}
}
StmtKind::AugAssign { target, .. } => {
self.handle_node_load(target);
}
StmtKind::AugAssign { target, .. } => self.handle_node_load(target),
_ => {}
}
visitor::walk_stmt(self, stmt);
ast_visitor::walk_stmt(self, stmt);
match &stmt.node {
StmtKind::ClassDef { .. }
| StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. } => {
self.pop_scope();
StmtKind::ClassDef { .. } => {
if let Some(scope) = self.scopes.pop() {
self.dead_scopes.push(scope);
}
}
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
let scope = self.scopes.last().expect("No current scope found.");
for (name, binding) in scope.values.iter() {
// TODO(charlie): Ignore if using `locals`.
if self.settings.select.contains(&CheckCode::F841)
&& binding.used.is_none()
&& name != "_"
&& name != "__tracebackhide__"
&& name != "__traceback_info__"
&& name != "__traceback_supplement__"
&& matches!(binding.kind, BindingKind::Assignment)
{
self.checks.push(Check {
kind: CheckKind::UnusedVariable(name.to_string()),
location: binding.location,
});
}
}
if let Some(scope) = self.scopes.pop() {
self.dead_scopes.push(scope);
}
}
_ => {}
};
if let StmtKind::ClassDef { name, .. } = &stmt.node {
self.add_binding(Binding {
kind: BindingKind::Definition,
name: name.clone(),
used: false,
location: stmt.location,
});
self.add_binding(
name.to_string(),
Binding {
kind: BindingKind::ClassDefinition,
used: None,
location: stmt.location,
},
);
}
}
fn visit_annotation(&mut self, expr: &Expr) {
let initial = self.in_annotation;
self.in_annotation = true;
self.visit_expr(expr);
self.visit_expr(expr, None);
self.in_annotation = initial;
}
fn visit_expr(&mut self, expr: &Expr) {
fn visit_expr(&mut self, expr: &Expr, parent: Option<&Stmt>) {
let initial = self.in_f_string;
match &expr.node {
ExprKind::Name { ctx, .. } => match ctx {
ExprContext::Load => self.handle_node_load(expr),
ExprContext::Store => self.handle_node_store(expr),
ExprContext::Del => {}
ExprContext::Store => self.handle_node_store(expr, parent),
ExprContext::Del => self.handle_node_delete(expr),
},
ExprKind::GeneratorExp { .. } => self.push_scope(Scope {
kind: Generator,
values: BTreeMap::new(),
}),
ExprKind::Lambda { .. } => self.push_scope(Scope {
kind: Function,
values: BTreeMap::new(),
}),
ExprKind::GeneratorExp { .. }
| ExprKind::ListComp { .. }
| ExprKind::DictComp { .. }
| ExprKind::SetComp { .. } => self.push_scope(Scope::new(ScopeKind::Generator)),
ExprKind::Lambda { .. } => self.push_scope(Scope::new(ScopeKind::Function)),
ExprKind::Yield { .. } | ExprKind::YieldFrom { .. } => {
let scope = self.scopes.last().expect("No current scope found.");
if self
.settings
.select
.contains(CheckKind::YieldOutsideFunction.code())
&& matches!(scope.kind, ScopeKind::Class)
|| matches!(scope.kind, ScopeKind::Module)
{
self.checks.push(Check {
kind: CheckKind::YieldOutsideFunction,
location: expr.location,
});
}
}
ExprKind::JoinedStr { values } => {
if !self.in_f_string
&& self
@@ -304,10 +359,14 @@ impl Visitor for Checker<'_> {
_ => {}
};
visitor::walk_expr(self, expr);
ast_visitor::walk_expr(self, expr);
match &expr.node {
ExprKind::GeneratorExp { .. } | ExprKind::Lambda { .. } => {
ExprKind::GeneratorExp { .. }
| ExprKind::ListComp { .. }
| ExprKind::DictComp { .. }
| ExprKind::SetComp { .. }
| ExprKind::Lambda { .. } => {
if let Some(scope) = self.scopes.pop() {
self.dead_scopes.push(scope);
}
@@ -319,6 +378,59 @@ impl Visitor for Checker<'_> {
};
}
fn visit_excepthandler(&mut self, excepthandler: &Excepthandler) {
match &excepthandler.node {
ExcepthandlerKind::ExceptHandler { name, .. } => match name {
Some(name) => {
let scope = self.scopes.last().expect("No current scope found.");
if scope.values.contains_key(name) {
self.handle_node_store(
&Expr::new(
excepthandler.location,
ExprKind::Name {
id: name.to_string(),
ctx: ExprContext::Store,
},
),
None,
);
}
let scope = self.scopes.last().expect("No current scope found.");
let prev_definition = scope.values.get(name).cloned();
self.handle_node_store(
&Expr::new(
excepthandler.location,
ExprKind::Name {
id: name.to_string(),
ctx: ExprContext::Store,
},
),
None,
);
walk_excepthandler(self, excepthandler);
let scope = self.scopes.last_mut().expect("No current scope found.");
if let Some(binding) = scope.values.remove(name) {
if self.settings.select.contains(&CheckCode::F841) && binding.used.is_none()
{
self.checks.push(Check {
kind: CheckKind::UnusedVariable(name.to_string()),
location: excepthandler.location,
});
}
}
if let Some(binding) = prev_definition {
scope.values.insert(name.to_string(), binding);
}
}
None => walk_excepthandler(self, excepthandler),
},
}
}
fn visit_arguments(&mut self, arguments: &Arguments) {
if self
.settings
@@ -340,31 +452,33 @@ impl Visitor for Checker<'_> {
}
// Search for duplicates.
let mut idents: BTreeSet<String> = BTreeSet::new();
let mut idents: BTreeSet<&str> = BTreeSet::new();
for arg in all_arguments {
let ident = &arg.node.arg;
if idents.contains(ident) {
if idents.contains(ident.as_str()) {
self.checks.push(Check {
kind: CheckKind::DuplicateArgumentName,
location: arg.location,
});
break;
}
idents.insert(ident.clone());
idents.insert(ident);
}
}
visitor::walk_arguments(self, arguments);
ast_visitor::walk_arguments(self, arguments);
}
fn visit_arg(&mut self, arg: &Arg) {
self.add_binding(Binding {
kind: BindingKind::Argument,
name: arg.node.arg.clone(),
used: false,
location: arg.location,
});
visitor::walk_arg(self, arg);
self.add_binding(
arg.node.arg.to_string(),
Binding {
kind: BindingKind::Argument,
used: None,
location: arg.location,
},
);
ast_visitor::walk_arg(self, arg);
}
}
@@ -378,72 +492,187 @@ impl Checker<'_> {
.push(self.scopes.pop().expect("Attempted to pop without scope."));
}
fn add_binding(&mut self, binding: Binding) {
// TODO(charlie): Don't treat annotations as assignments if there is an existing value.
fn bind_builtins(&mut self) {
for builtin in BUILTINS {
self.add_binding(
builtin.to_string(),
Binding {
kind: BindingKind::Builtin,
location: Default::default(),
used: None,
},
)
}
for builtin in MAGIC_GLOBALS {
self.add_binding(
builtin.to_string(),
Binding {
kind: BindingKind::Builtin,
location: Default::default(),
used: None,
},
)
}
}
fn add_binding(&mut self, name: String, binding: Binding) {
let scope = self.scopes.last_mut().expect("No current scope found.");
scope.values.insert(
binding.name.clone(),
match scope.values.get(&binding.name) {
None => binding,
Some(existing) => Binding {
kind: binding.kind,
name: binding.name,
location: binding.location,
used: existing.used,
},
// TODO(charlie): Don't treat annotations as assignments if there is an existing value.
let binding = match scope.values.get(&name) {
None => binding,
Some(existing) => Binding {
kind: binding.kind,
location: binding.location,
used: existing.used,
},
);
};
scope.values.insert(name, binding);
}
fn handle_node_load(&mut self, expr: &Expr) {
if let ExprKind::Name { id, .. } = &expr.node {
let scope_id = self.scopes.last_mut().expect("No current scope found.").id;
let mut first_iter = true;
let mut in_generators = false;
for scope in self.scopes.iter_mut().rev() {
if matches!(scope.kind, Class) {
if matches!(scope.kind, ScopeKind::Class) {
if id == "__class__" {
return;
} else {
} else if !first_iter && !in_generators {
continue;
}
}
if let Some(binding) = scope.values.get_mut(id) {
binding.used = true;
binding.used = Some(scope_id);
return;
}
first_iter = false;
in_generators = matches!(scope.kind, ScopeKind::Generator);
}
if self.settings.select.contains(&CheckCode::F821) {
self.checks.push(Check {
kind: CheckKind::UndefinedName(id.clone()),
location: expr.location,
})
}
}
}
fn handle_node_store(&mut self, expr: &Expr) {
fn handle_node_store(&mut self, expr: &Expr, parent: Option<&Stmt>) {
if let ExprKind::Name { id, .. } = &expr.node {
let current = self.scopes.last().expect("No current scope found.");
if self.settings.select.contains(&CheckCode::F823)
&& matches!(current.kind, ScopeKind::Function)
&& !current.values.contains_key(id)
{
for scope in self.scopes.iter().rev().skip(1) {
if matches!(scope.kind, ScopeKind::Function)
|| matches!(scope.kind, ScopeKind::Module)
{
let used = scope
.values
.get(id)
.map(|binding| binding.used)
.unwrap_or_default();
if let Some(scope_id) = used {
if scope_id == current.id {
self.checks.push(Check {
kind: CheckKind::UndefinedLocal(id.clone()),
location: expr.location,
});
}
}
}
}
}
// TODO(charlie): Handle alternate binding types (like `Annotation`).
self.add_binding(Binding {
kind: BindingKind::Assignment,
name: id.to_string(),
used: false,
location: expr.location,
});
if id == "__all__"
&& matches!(current.kind, ScopeKind::Module)
&& match parent {
None => false,
Some(stmt) => {
matches!(stmt.node, StmtKind::Assign { .. })
|| matches!(stmt.node, StmtKind::AugAssign { .. })
|| matches!(stmt.node, StmtKind::AnnAssign { .. })
}
}
{
// Really need parent here.
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Export(extract_all_names(parent.unwrap(), current)),
used: None,
location: expr.location,
},
);
} else {
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Assignment,
used: None,
location: expr.location,
},
);
}
}
}
fn handle_node_delete(&mut self, expr: &Expr) {
if let ExprKind::Name { id, .. } = &expr.node {
let current = self.scopes.last_mut().expect("No current scope found.");
if current.values.remove(id).is_none()
&& self.settings.select.contains(&CheckCode::F821)
{
self.checks.push(Check {
kind: CheckKind::UndefinedName(id.clone()),
location: expr.location,
})
}
}
}
fn check_deferred(&mut self, path: &str) {
for value in self.deferred.clone() {
if let Ok(expr) = &parser::parse_expression(&value, path) {
self.visit_expr(expr);
self.visit_expr(expr, None);
}
}
}
fn check_dead_scopes(&mut self) {
if self.settings.select.contains(&CheckCode::F401) {
// TODO(charlie): Handle `__all__`.
for scope in &self.dead_scopes {
for (_, binding) in scope.values.iter().rev() {
if !binding.used {
if let BindingKind::Importation(name) = &binding.kind {
self.checks.push(Check {
kind: CheckKind::UnusedImport(name.clone()),
location: binding.location,
});
let all_binding = match scope.values.get("__all__") {
Some(binding) => match &binding.kind {
BindingKind::Export(names) => Some(names),
_ => None,
},
_ => None,
};
for (name, binding) in scope.values.iter().rev() {
let used = binding.used.is_some()
|| all_binding
.map(|names| names.contains(name))
.unwrap_or_default();
if !used {
match &binding.kind {
BindingKind::Importation(full_name)
| BindingKind::SubmoduleImportation(full_name) => {
self.checks.push(Check {
kind: CheckKind::UnusedImport(full_name.to_string()),
location: binding.location,
});
}
_ => {}
}
}
}
@@ -454,10 +683,8 @@ impl Checker<'_> {
pub fn check_ast(python_ast: &Suite, settings: &Settings, path: &str) -> Vec<Check> {
let mut checker = Checker::new(settings);
checker.push_scope(Scope {
kind: Module,
values: BTreeMap::new(),
});
checker.push_scope(Scope::new(ScopeKind::Module));
checker.bind_builtins();
for stmt in python_ast {
checker.visit_stmt(stmt);

164
src/check_cst.rs Normal file
View File

@@ -0,0 +1,164 @@
use std::borrow::Borrow;
use std::collections::BTreeMap;
use std::sync::Arc;
use bat::PrettyPrinter;
use bumpalo::Bump;
use libcst_native::{
Arg, ClassDef, Codegen, Expression, FormattedStringContent, If, Module, SimpleString,
};
use rustpython_parser::ast::Location;
use crate::checks::{Check, CheckKind};
use crate::cst_visitor;
use crate::cst_visitor::CSTVisitor;
use crate::settings::Settings;
enum ScopeKind {
Class,
Function,
Generator,
Module,
}
struct Scope {
kind: ScopeKind,
values: BTreeMap<String, Binding>,
}
enum BindingKind {
Argument,
Assignment,
ClassDefinition,
Definition,
FutureImportation,
Importation,
StarImportation,
SubmoduleImportation,
}
struct Binding {
kind: BindingKind,
name: String,
location: Location,
used: bool,
}
struct Checker<'a> {
settings: &'a Settings,
checks: Vec<Check>,
arena: Vec<String>,
}
impl Checker<'_> {
pub fn new(settings: &Settings) -> Checker {
Checker {
settings,
checks: vec![],
arena: vec![],
}
}
}
const QUOTE: &str = "\"";
impl<'b> CSTVisitor for Checker<'_> {
fn visit_Expression<'a>(&mut self, node: &'a Expression<'a>) -> Expression<'a> {
match node {
Expression::FormattedString(node) => match &node.parts[..] {
[node] => match node {
FormattedStringContent::Text(node) => {
self.arena.push(format!("\"{}\"", node.value));
return Expression::SimpleString(Box::new(SimpleString {
value: node.value,
lpar: vec![],
rpar: vec![],
}));
}
_ => {}
},
_ => {}
},
_ => {}
}
cst_visitor::walk_Expression(self, node)
}
fn visit_ClassDef<'a>(&mut self, node: &'a ClassDef<'a>) -> ClassDef<'a> {
let mut bases: Vec<Arg<'a>> = node
.bases
.clone()
.into_iter()
.filter(|node| {
if let Expression::Name(node) = &node.value {
node.value != "object"
} else {
true
}
})
.collect();
let mut transformed: ClassDef<'a> = node.clone();
if bases.is_empty() {
transformed.lpar = None;
transformed.rpar = None;
} else {
let node = bases.last_mut().unwrap();
node.comma = None;
}
transformed.bases = bases;
transformed
}
fn visit_If(&mut self, node: &If) {
if let Expression::Tuple { .. } = node.test {
self.checks.push(Check {
kind: CheckKind::IfTuple,
location: Default::default(),
});
}
cst_visitor::walk_If(self, node);
}
}
pub fn check_cst<'a>(python_cst: &'a Module<'a>, settings: &Settings) -> Vec<Check> {
// // Create a new arena to bump allocate into.
// let bump = Bump::new();
//
// // Allocate values into the arena.
// let scooter = bump.alloc(python_cst.clone());
let mut x = python_cst.clone();
let mut s = Default::default();
x.codegen(&mut s);
println!("Starting from source:");
println!("```");
let source = s.to_string().into_bytes();
PrettyPrinter::new()
.input_from_bytes(&source)
.language("python")
.print()
.unwrap();
println!("```");
let mut checker = Checker::new(settings);
let mut transformed = checker.visit_Module(python_cst);
let mut state = Default::default();
transformed.codegen(&mut state);
println!("");
println!("Generated output:");
println!("```");
let source = state.to_string().into_bytes();
PrettyPrinter::new()
.input_from_bytes(&source)
.language("python")
.print()
.unwrap();
println!("```");
checker.checks
}

View File

@@ -3,24 +3,37 @@ use rustpython_parser::ast::Location;
use crate::checks::{Check, CheckKind};
use crate::settings::Settings;
pub fn check_lines(contents: &str, settings: &Settings) -> Vec<Check> {
contents
.lines()
.enumerate()
.filter_map(|(row, line)| {
if settings.select.contains(CheckKind::LineTooLong.code())
&& line.len() > settings.line_length
{
let chunks: Vec<&str> = line.split_whitespace().collect();
if !(chunks.len() == 1 || (chunks.len() == 2 && chunks[0] == "#")) {
return Some(Check {
kind: CheckKind::LineTooLong,
location: Location::new(row + 1, settings.line_length + 1),
});
pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings) {
let enforce_line_too_ling = settings.select.contains(CheckKind::LineTooLong.code());
let mut line_checks = vec![];
let mut ignored = vec![];
for (row, line) in contents.lines().enumerate() {
// Remove any ignored checks.
// TODO(charlie): Only validate checks for the current line.
for (index, check) in checks.iter().enumerate() {
if check.location.row() == row + 1 && check.is_inline_ignored(line) {
ignored.push(index);
}
}
// Enforce line length.
if enforce_line_too_ling && line.len() > settings.line_length {
let chunks: Vec<&str> = line.split_whitespace().collect();
if !(chunks.len() == 1 || (chunks.len() == 2 && chunks[0] == "#")) {
let check = Check {
kind: CheckKind::LineTooLong,
location: Location::new(row + 1, settings.line_length + 1),
};
if !check.is_inline_ignored(line) {
line_checks.push(check);
}
}
None
})
.collect()
}
}
ignored.sort();
for index in ignored.iter().rev() {
checks.swap_remove(*index);
}
checks.extend(line_checks);
}

View File

@@ -1,6 +1,8 @@
use std::str::FromStr;
use anyhow::Result;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
@@ -11,8 +13,12 @@ pub enum CheckCode {
F403,
F541,
F634,
F704,
F706,
F821,
F823,
F831,
F841,
F901,
}
@@ -26,8 +32,12 @@ impl FromStr for CheckCode {
"F403" => Ok(CheckCode::F403),
"F541" => Ok(CheckCode::F541),
"F634" => Ok(CheckCode::F634),
"F704" => Ok(CheckCode::F704),
"F706" => Ok(CheckCode::F706),
"F821" => Ok(CheckCode::F821),
"F823" => Ok(CheckCode::F823),
"F831" => Ok(CheckCode::F831),
"F841" => Ok(CheckCode::F841),
"F901" => Ok(CheckCode::F901),
_ => Err(anyhow::anyhow!("Unknown check code: {s}")),
}
@@ -42,8 +52,12 @@ impl CheckCode {
CheckCode::F403 => "F403",
CheckCode::F541 => "F541",
CheckCode::F634 => "F634",
CheckCode::F704 => "F704",
CheckCode::F706 => "F706",
CheckCode::F821 => "F821",
CheckCode::F823 => "F823",
CheckCode::F831 => "F831",
CheckCode::F841 => "F841",
CheckCode::F901 => "F901",
}
}
@@ -56,8 +70,12 @@ impl CheckCode {
CheckCode::F403 => &LintSource::AST,
CheckCode::F541 => &LintSource::AST,
CheckCode::F634 => &LintSource::AST,
CheckCode::F704 => &LintSource::AST,
CheckCode::F706 => &LintSource::AST,
CheckCode::F821 => &LintSource::AST,
CheckCode::F823 => &LintSource::AST,
CheckCode::F831 => &LintSource::AST,
CheckCode::F841 => &LintSource::AST,
CheckCode::F901 => &LintSource::AST,
}
}
@@ -78,7 +96,11 @@ pub enum CheckKind {
LineTooLong,
RaiseNotImplemented,
ReturnOutsideFunction,
UndefinedLocal(String),
UndefinedName(String),
UnusedImport(String),
UnusedVariable(String),
YieldOutsideFunction,
}
impl CheckKind {
@@ -92,7 +114,11 @@ impl CheckKind {
CheckKind::LineTooLong => &CheckCode::E501,
CheckKind::RaiseNotImplemented => &CheckCode::F901,
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
CheckKind::UndefinedName(_) => &CheckCode::F821,
CheckKind::UnusedImport(_) => &CheckCode::F401,
CheckKind::UnusedVariable(_) => &CheckCode::F841,
CheckKind::YieldOutsideFunction => &CheckCode::F704,
}
}
@@ -105,9 +131,7 @@ impl CheckKind {
CheckKind::FStringMissingPlaceholders => {
"f-string without any placeholders".to_string()
}
CheckKind::IfTuple => {
"If test is a tuple.to_string(), which is always `True`".to_string()
}
CheckKind::IfTuple => "If test is a tuple, which is always `True`".to_string(),
CheckKind::ImportStarUsage => "Unable to detect undefined names".to_string(),
CheckKind::LineTooLong => "Line too long".to_string(),
CheckKind::RaiseNotImplemented => {
@@ -116,7 +140,19 @@ impl CheckKind {
CheckKind::ReturnOutsideFunction => {
"a `return` statement outside of a function/method".to_string()
}
CheckKind::UndefinedName(name) => {
format!("Undefined name `{name}`")
}
CheckKind::UndefinedLocal(name) => {
format!("Local variable `{name}` referenced before assignment")
}
CheckKind::UnusedImport(name) => format!("`{name}` imported but unused"),
CheckKind::UnusedVariable(name) => {
format!("Local variable `{name}` is assigned to but never used")
}
CheckKind::YieldOutsideFunction => {
"a `yield` or `yield from` statement outside of a function/method".to_string()
}
}
}
}
@@ -126,3 +162,31 @@ pub struct Check {
pub kind: CheckKind,
pub location: Location,
}
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?").expect("Invalid regex")
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").expect("Invalid regex"));
impl Check {
pub fn is_inline_ignored(&self, line: &str) -> bool {
match NO_QA_REGEX.captures(line) {
Some(caps) => match caps.name("codes") {
Some(codes) => {
for code in SPLIT_COMMA_REGEX
.split(codes.as_str())
.map(|code| code.trim())
.filter(|code| !code.is_empty())
{
if code == self.kind.code().as_str() {
return true;
}
}
false
}
None => true,
},
None => false,
}
}
}

1039
src/cst_visitor.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,39 @@
use std::fs::File;
use std::io::{BufRead, BufReader, Read};
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use anyhow::Result;
use glob::Pattern;
use walkdir::{DirEntry, WalkDir};
fn is_not_hidden(entry: &DirEntry) -> bool {
entry
.file_name()
.to_str()
.map(|s| entry.depth() == 0 || !s.starts_with('.'))
.map(|s| (entry.depth() == 0 || !s.starts_with('.')))
.unwrap_or(false)
}
pub fn iter_python_files(path: &PathBuf) -> impl Iterator<Item = DirEntry> {
fn is_not_excluded(entry: &DirEntry, exclude: &[Pattern]) -> bool {
entry
.path()
.to_str()
.map(|s| !exclude.iter().any(|pattern| pattern.matches(s)))
.unwrap_or(false)
}
pub fn iter_python_files<'a>(
path: &'a PathBuf,
exclude: &'a [Pattern],
) -> impl Iterator<Item = DirEntry> + 'a {
WalkDir::new(path)
.follow_links(true)
.into_iter()
.filter_entry(is_not_hidden)
.filter_entry(|entry| is_not_hidden(entry) && is_not_excluded(entry, exclude))
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().to_string_lossy().ends_with(".py"))
}
pub fn read_line(path: &Path, row: &usize) -> Result<String> {
let file = File::open(path)?;
let buf_reader = BufReader::new(file);
buf_reader
.lines()
.nth(*row - 1)
.unwrap()
.map_err(|e| e.into())
}
pub fn read_file(path: &Path) -> Result<String> {
let file = File::open(path)?;
let mut buf_reader = BufReader::new(file);

View File

@@ -1,11 +1,15 @@
mod ast_ops;
mod ast_visitor;
mod builtins;
mod cache;
pub mod check_ast;
mod check_cst;
mod check_lines;
pub mod checks;
mod cst_visitor;
pub mod fs;
pub mod linter;
pub mod logging;
pub mod message;
mod pyproject;
pub mod settings;
mod visitor;

View File

@@ -2,21 +2,19 @@ use std::path::Path;
use anyhow::Result;
use log::debug;
use rustpython_parser::parser;
use crate::check_ast::check_ast;
use crate::check_lines::check_lines;
use crate::checks::{Check, LintSource};
use crate::check_cst::check_cst;
use crate::checks::Check;
use crate::message::Message;
use crate::settings::Settings;
use crate::{cache, fs};
pub fn check_path(path: &Path, settings: &Settings, mode: &cache::Mode) -> Result<Vec<Message>> {
// Check the cache.
if let Some(messages) = cache::get(path, settings, mode) {
debug!("Cache hit for: {}", path.to_string_lossy());
return Ok(messages);
}
// // Check the cache.
// if let Some(messages) = cache::get(path, settings, mode) {
// debug!("Cache hit for: {}", path.to_string_lossy());
// return Ok(messages);
// }
// Read the file from disk.
let contents = fs::read_file(path)?;
@@ -24,39 +22,44 @@ pub fn check_path(path: &Path, settings: &Settings, mode: &cache::Mode) -> Resul
// Aggregate all checks.
let mut checks: Vec<Check> = vec![];
// Run the AST-based checks.
if settings
.select
.iter()
.any(|check_code| matches!(check_code.lint_source(), LintSource::AST))
{
let path = path.to_string_lossy();
let python_ast = parser::parse_program(&contents, &path)?;
checks.extend(check_ast(&python_ast, settings, &path));
}
// Run the CST-based checks.
let _ = match libcst_native::parse_module(&contents, None) {
Ok(m) => m,
Err(e) => {
return Err(anyhow::anyhow!("Failed to parse"));
}
};
// Run the lines-based checks.
if settings
.select
.iter()
.any(|check_code| matches!(check_code.lint_source(), LintSource::Lines))
{
checks.extend(check_lines(&contents, settings));
}
Ok(vec![])
// checks.extend(check_cst(&python_cst, settings));
// Convert to messages.
let messages: Vec<Message> = checks
.into_iter()
.map(|check| Message {
kind: check.kind,
location: check.location,
filename: path.to_string_lossy().to_string(),
})
.filter(|message| !message.is_inline_ignored())
.collect();
cache::set(path, settings, &messages, mode);
Ok(messages)
// // Run the AST-based checks.
// if settings
// .select
// .iter()
// .any(|check_code| matches!(check_code.lint_source(), LintSource::AST))
// {
// let path = path.to_string_lossy();
// let python_ast = parser::parse_program(&contents, &path)?;
// checks.extend(check_ast(&python_ast, settings, &path));
// }
//
// // Run the lines-based checks.
// check_lines(&mut checks, &contents, settings);
//
// // Convert to messages.
// let messages: Vec<Message> = checks
// .into_iter()
// .map(|check| Message {
// kind: check.kind,
// location: check.location,
// filename: path.to_string_lossy().to_string(),
// })
// .collect();
//
// cache::set(path, settings, &messages, mode);
//
// Ok(messages)
}
#[cfg(test)]
@@ -108,14 +111,19 @@ mod tests {
&cache::Mode::None,
)?;
let expected = vec![
Message {
kind: CheckKind::UnusedImport("logging.handlers".to_string()),
location: Location::new(12, 1),
filename: "./resources/test/src/F401.py".to_string(),
},
Message {
kind: CheckKind::UnusedImport("functools".to_string()),
location: Location::new(2, 1),
location: Location::new(3, 1),
filename: "./resources/test/src/F401.py".to_string(),
},
Message {
kind: CheckKind::UnusedImport("collections.OrderedDict".to_string()),
location: Location::new(3, 1),
location: Location::new(4, 1),
filename: "./resources/test/src/F401.py".to_string(),
},
];
@@ -224,6 +232,42 @@ mod tests {
Ok(())
}
#[test]
fn f704() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F704.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F704]),
},
&cache::Mode::None,
)?;
let expected = vec![
Message {
kind: CheckKind::YieldOutsideFunction,
location: Location::new(6, 5),
filename: "./resources/test/src/F704.py".to_string(),
},
Message {
kind: CheckKind::YieldOutsideFunction,
location: Location::new(9, 1),
filename: "./resources/test/src/F704.py".to_string(),
},
Message {
kind: CheckKind::YieldOutsideFunction,
location: Location::new(10, 1),
filename: "./resources/test/src/F704.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f706() -> Result<()> {
let actual = check_path(
@@ -255,6 +299,47 @@ mod tests {
Ok(())
}
#[test]
fn f821() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F821.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F821]),
},
&cache::Mode::None,
)?;
let expected = vec![
Message {
kind: CheckKind::UndefinedName("self".to_string()),
location: Location::new(2, 12),
filename: "./resources/test/src/F821.py".to_string(),
},
Message {
kind: CheckKind::UndefinedName("self".to_string()),
location: Location::new(6, 13),
filename: "./resources/test/src/F821.py".to_string(),
},
Message {
kind: CheckKind::UndefinedName("self".to_string()),
location: Location::new(10, 9),
filename: "./resources/test/src/F821.py".to_string(),
},
Message {
kind: CheckKind::UndefinedName("numeric_string".to_string()),
location: Location::new(21, 12),
filename: "./resources/test/src/F821.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f831() -> Result<()> {
let actual = check_path(
@@ -291,6 +376,61 @@ mod tests {
Ok(())
}
#[test]
fn f823() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F823.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F823]),
},
&cache::Mode::None,
)?;
let expected = vec![Message {
kind: CheckKind::UndefinedLocal("my_var".to_string()),
location: Location::new(6, 5),
filename: "./resources/test/src/F823.py".to_string(),
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f841() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F841.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F841]),
},
&cache::Mode::None,
)?;
let expected = vec![
Message {
kind: CheckKind::UnusedVariable("e".to_string()),
location: Location::new(3, 1),
filename: "./resources/test/src/F841.py".to_string(),
},
Message {
kind: CheckKind::UnusedVariable("z".to_string()),
location: Location::new(16, 5),
filename: "./resources/test/src/F841.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f901() -> Result<()> {
let actual = check_path(

View File

@@ -51,19 +51,16 @@ struct Cli {
fn run_once(files: &[PathBuf], settings: &Settings, cache: bool) -> Result<Vec<Message>> {
// Collect all the files to check.
let start = Instant::now();
let files: Vec<DirEntry> = files.iter().flat_map(iter_python_files).collect();
let files: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude))
.collect();
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let messages: Vec<Message> = files
let mut messages: Vec<Message> = files
.par_iter()
.filter(|entry| {
!settings
.exclude
.iter()
.any(|exclusion| entry.path().starts_with(exclusion))
})
.map(|entry| {
check_path(entry.path(), settings, &cache.into()).unwrap_or_else(|e| {
error!("Failed to check {}: {e:?}", entry.path().to_string_lossy());
@@ -72,6 +69,7 @@ fn run_once(files: &[PathBuf], settings: &Settings, cache: bool) -> Result<Vec<M
})
.flatten()
.collect();
messages.sort_unstable();
let duration = start.elapsed();
debug!("Checked files in: {:?}", duration);
@@ -79,7 +77,7 @@ fn run_once(files: &[PathBuf], settings: &Settings, cache: bool) -> Result<Vec<M
}
fn report_once(messages: &[Message]) -> Result<()> {
println!("Found {} error(s).", messages.len());
// println!("Found {} error(s).", messages.len());
if !messages.is_empty() {
println!();

View File

@@ -1,13 +1,11 @@
use std::cmp::Ordering;
use std::fmt;
use std::path::Path;
use colored::Colorize;
use regex::Regex;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
use crate::checks::CheckKind;
use crate::fs;
#[derive(Serialize, Deserialize)]
#[serde(remote = "Location")]
@@ -32,35 +30,19 @@ pub struct Message {
pub filename: String,
}
impl Message {
pub fn is_inline_ignored(&self) -> bool {
match fs::read_line(Path::new(&self.filename), &self.location.row()) {
Ok(line) => {
// https://github.com/PyCQA/flake8/blob/799c71eeb61cf26c7c176aed43e22523e2a6d991/src/flake8/defaults.py#L26
let re = Regex::new(r"(?i)# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?")
.unwrap();
match re.captures(&line) {
Some(caps) => match caps.name("codes") {
Some(codes) => {
let re = Regex::new(r"[,\s]").unwrap();
for code in re
.split(codes.as_str())
.map(|code| code.trim())
.filter(|code| !code.is_empty())
{
if code == self.kind.code().as_str() {
return true;
}
}
false
}
None => true,
},
None => false,
}
}
Err(_) => false,
}
impl Ord for Message {
fn cmp(&self, other: &Self) -> Ordering {
(&self.filename, self.location.row(), self.location.column()).cmp(&(
&other.filename,
other.location.row(),
self.location.column(),
))
}
}
impl PartialOrd for Message {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

View File

@@ -14,12 +14,20 @@ pub fn load_config<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Result<(Pat
Some(project_root) => match find_pyproject_toml(&project_root) {
Some(path) => {
debug!("Found pyproject.toml at: {}", path.to_string_lossy());
let pyproject = parse_pyproject_toml(&path)?;
let config = pyproject
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default();
Ok((project_root, config))
match parse_pyproject_toml(&path) {
Ok(pyproject) => {
let config = pyproject
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default();
Ok((project_root, config))
}
Err(e) => {
println!("Failed to load pyproject.toml: {:?}", e);
println!("Falling back to default configuration...");
Ok(Default::default())
}
}
}
None => Ok(Default::default()),
},
@@ -225,15 +233,22 @@ other-attribute = 1
config,
Config {
line_length: Some(88),
exclude: Some(vec![Path::new("excluded.py").to_path_buf()]),
exclude: Some(vec![
Path::new("excluded.py").to_path_buf(),
Path::new("**/migrations").to_path_buf()
]),
select: Some(BTreeSet::from([
CheckCode::E501,
CheckCode::F401,
CheckCode::F403,
CheckCode::F541,
CheckCode::F634,
CheckCode::F704,
CheckCode::F706,
CheckCode::F821,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
])),
}

View File

@@ -1,8 +1,9 @@
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::path::Path;
use anyhow::Result;
use glob::Pattern;
use crate::checks::CheckCode;
use crate::pyproject::load_config;
@@ -10,7 +11,7 @@ use crate::pyproject::load_config;
#[derive(Debug)]
pub struct Settings {
pub line_length: usize,
pub exclude: Vec<PathBuf>,
pub exclude: Vec<Pattern>,
pub select: BTreeSet<CheckCode>,
}
@@ -39,6 +40,7 @@ impl Settings {
path
}
})
.map(|path| Pattern::new(&path.to_string_lossy()).expect("Invalid pattern."))
.collect(),
select: config.select.unwrap_or_else(|| {
BTreeSet::from([
@@ -49,6 +51,7 @@ impl Settings {
CheckCode::F634,
CheckCode::F706,
CheckCode::F831,
CheckCode::F823,
CheckCode::F901,
])
}),