Compare commits

..

1 Commits

Author SHA1 Message Date
Charlie Marsh
8c876f319b Only log fix failures in debug 2023-08-21 18:12:49 -04:00
873 changed files with 47728 additions and 48702 deletions

136
.github/workflows/benchmark.yaml vendored Normal file
View File

@@ -0,0 +1,136 @@
name: Benchmark
on:
pull_request:
paths:
- "Cargo.toml"
- "Cargo.lock"
- "rust-toolchain"
- "crates/**"
- "!crates/ruff_dev"
- "!crates/ruff_shrinking"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
run-benchmark:
if: github.event_name == 'pull_request'
name: "Run | ${{ matrix.os }}"
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: "PR - Checkout Branch"
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: "PR - Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v2
- name: "PR - Build benchmarks"
run: cargo bench -p ruff_benchmark --no-run
- name: "PR - Run benchmarks"
run: cargo benchmark --save-baseline=pr
- name: "Main - Checkout Branch"
uses: actions/checkout@v3
with:
clean: false
ref: main
- name: "Main - Install Rust toolchain"
run: rustup show
- name: "Main - Build benchmarks"
run: cargo bench -p ruff_benchmark --no-run
- name: "Main - Run benchmarks"
run: cargo benchmark --save-baseline=main
- name: "Upload benchmark results"
uses: actions/upload-artifact@v3
with:
name: benchmark-results-${{ matrix.os }}
path: ./target/criterion
# Cleanup
- name: Remove Criterion Artifact
uses: JesseTG/rm@v1.0.3
with:
path: ./target/criterion
benchmark-compare:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
name: Compare
needs:
- run-benchmark
steps:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install critcmp"
uses: taiki-e/install-action@v2
with:
tool: critcmp
- name: "Linux | Download PR benchmark results"
uses: actions/download-artifact@v3
with:
name: benchmark-results-ubuntu-latest
path: ./target/criterion
- name: "Linux | Compare benchmark results"
shell: bash
run: |
echo "### Benchmark" >> summary.md
echo "#### Linux" >> summary.md
echo "\`\`\`" >> summary.md
critcmp main pr >> summary.md
echo "\`\`\`" >> summary.md
echo "" >> summary.md
- name: "Linux | Cleanup benchmark results"
run: rm -rf ./target/criterion
- name: "Windows | Download PR benchmark results"
uses: actions/download-artifact@v3
with:
name: benchmark-results-windows-latest
path: ./target/criterion
- name: "Windows | Compare benchmark results"
shell: bash
run: |
echo "#### Windows" >> summary.md
echo "\`\`\`" >> summary.md
critcmp main pr >> summary.md
echo "\`\`\`" >> summary.md
echo "" >> summary.md
echo ${{ github.event.pull_request.number }} > pr-number
cat summary.md > $GITHUB_STEP_SUMMARY
- uses: actions/upload-artifact@v3
name: Upload PR Number
with:
name: pr-number
path: pr-number
- uses: actions/upload-artifact@v3
name: Upload Summary
with:
name: summary
path: summary.md

View File

@@ -30,7 +30,7 @@ jobs:
with:
fetch-depth: 0
- uses: tj-actions/changed-files@v38
- uses: tj-actions/changed-files@v37
id: changed
with:
files_yaml: |
@@ -106,9 +106,10 @@ jobs:
shell: bash
# We can't reject unreferenced snapshots on windows because flake8_executable can't run on windows
run: cargo insta test --all --all-features
# Check linter fix compatibility with Black
- name: "Check Black compatibility"
run: cargo test --package ruff_cli --test black_compatibility_test -- --ignored
- run: cargo test --package ruff_cli --test black_compatibility_test -- --ignored
# TODO: Skipped as it's currently broken. The resource were moved from the
# ruff_cli to ruff crate, but this test was not updated.
if: false
# Check for broken links in the documentation.
- run: cargo doc --all --no-deps
env:
@@ -340,28 +341,3 @@ jobs:
run: cat target/progress_projects_stats.txt > $GITHUB_STEP_SUMMARY
- name: "Remove checkouts from cache"
run: rm -r target/progress_projects
benchmarks:
runs-on: ubuntu-latest
steps:
- name: "Checkout Branch"
uses: actions/checkout@v3
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@v2
with:
tool: cargo-codspeed
- uses: Swatinem/rust-cache@v2
- name: "Build benchmarks"
run: cargo codspeed build --features codspeed -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@v1
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@@ -2,7 +2,7 @@ name: PR Check Comment
on:
workflow_run:
workflows: [CI]
workflows: [CI, Benchmark]
types: [completed]
workflow_dispatch:
inputs:
@@ -43,34 +43,42 @@ jobs:
path: pr/ecosystem
if_no_artifact_found: ignore
- uses: dawidd6/action-download-artifact@v2
name: "Download Benchmark Result"
id: download-benchmark-result
if: steps.pr-number.outputs.pr-number
with:
name: summary
workflow: benchmark.yaml
pr: ${{ steps.pr-number.outputs.pr-number }}
path: pr/benchmark
if_no_artifact_found: ignore
- name: Generate Comment
id: generate-comment
if: steps.download-ecosystem-result.outputs.found_artifact == 'true'
if: steps.download-ecosystem-result.outputs.found_artifact == 'true' || steps.download-benchmark-result.outputs.found_artifact == 'true'
run: |
echo '## PR Check Results' >> comment.txt
echo "### Ecosystem" >> comment.txt
cat pr/ecosystem/ecosystem-result >> comment.txt
echo "" >> comment.txt
echo 'comment<<EOF' >> $GITHUB_OUTPUT
cat comment.txt >> $GITHUB_OUTPUT
echo '## PR Check Results' >> $GITHUB_OUTPUT
if [[ -f pr/ecosystem/ecosystem-result ]]
then
echo "### Ecosystem" >> $GITHUB_OUTPUT
cat pr/ecosystem/ecosystem-result >> $GITHUB_OUTPUT
echo "" >> $GITHUB_OUTPUT
fi
if [[ -f pr/benchmark/summary.md ]]
then
cat pr/benchmark/summary.md >> $GITHUB_OUTPUT
fi
echo 'EOF' >> $GITHUB_OUTPUT
- name: Find Comment
uses: peter-evans/find-comment@v2
if: steps.generate-comment.outcome == 'success'
id: find-comment
with:
issue-number: ${{ steps.pr-number.outputs.pr-number }}
comment-author: "github-actions[bot]"
body-includes: PR Check Results
- name: Create or update comment
if: steps.find-comment.outcome == 'success'
uses: peter-evans/create-or-update-comment@v3
if: steps.generate-comment.outputs.comment
uses: thollander/actions-comment-pull-request@v2
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.pr-number }}
body-path: comment.txt
edit-mode: replace
pr_number: ${{ steps.pr-number.outputs.pr-number }}
message: ${{ steps.generate-comment.outputs.comment }}
comment_tag: PR Check Results

103
Cargo.lock generated
View File

@@ -414,28 +414,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "codspeed"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeec2fbed4969dc38b5ca201115dd5c2614b8ef78e0a7221dd5f0977fb1552b"
dependencies = [
"colored",
"libc",
"serde_json",
]
[[package]]
name = "codspeed-criterion-compat"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b13f0a08d40ce7c95bdf288f725b975e62fcadfa8ba152340943bab6de43af7"
dependencies = [
"codspeed",
"colored",
"criterion",
]
[[package]]
name = "colorchoice"
version = "1.0.0"
@@ -834,20 +812,15 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.286"
version = "0.0.285"
dependencies = [
"anyhow",
"clap",
"colored",
"configparser",
"itertools",
"log",
"once_cell",
"pep440_rs",
"pretty_assertions",
"regex",
"ruff",
"ruff_workspace",
"rustc-hash",
"serde",
"serde_json",
@@ -903,10 +876,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -2093,7 +2064,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.286"
version = "0.0.285"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2101,9 +2072,11 @@ dependencies = [
"chrono",
"clap",
"colored",
"dirs 5.0.1",
"fern",
"glob",
"globset",
"ignore",
"imperative",
"insta",
"is-macro",
@@ -2143,6 +2116,7 @@ dependencies = [
"serde",
"serde_json",
"serde_with",
"shellexpand",
"similar",
"smallvec",
"strum",
@@ -2154,7 +2128,6 @@ dependencies = [
"typed-arena",
"unicode-width",
"unicode_names2",
"uuid",
"wsl",
]
@@ -2162,7 +2135,6 @@ dependencies = [
name = "ruff_benchmark"
version = "0.0.0"
dependencies = [
"codspeed-criterion-compat",
"criterion",
"mimalloc",
"once_cell",
@@ -2192,7 +2164,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.286"
version = "0.0.285"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2210,7 +2182,6 @@ dependencies = [
"glob",
"ignore",
"insta",
"is-macro",
"itertools",
"itoa",
"log",
@@ -2222,7 +2193,6 @@ dependencies = [
"ruff",
"ruff_cache",
"ruff_diagnostics",
"ruff_formatter",
"ruff_macros",
"ruff_python_ast",
"ruff_python_formatter",
@@ -2230,7 +2200,6 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
"rustc-hash",
"serde",
"serde_json",
@@ -2238,9 +2207,7 @@ dependencies = [
"similar",
"strum",
"tempfile",
"thiserror",
"tikv-jemallocator",
"tracing",
"ureq",
"walkdir",
"wild",
@@ -2273,7 +2240,6 @@ dependencies = [
"ruff_python_parser",
"ruff_python_stdlib",
"ruff_python_trivia",
"ruff_workspace",
"schemars",
"serde",
"serde_json",
@@ -2374,7 +2340,6 @@ dependencies = [
"insta",
"is-macro",
"itertools",
"memchr",
"once_cell",
"ruff_formatter",
"ruff_python_ast",
@@ -2434,7 +2399,6 @@ dependencies = [
"ruff_text_size",
"rustc-hash",
"static_assertions",
"test-case",
"tiny-keccak",
"unic-emoji-char",
"unic-ucd-ident",
@@ -2533,48 +2497,18 @@ dependencies = [
"log",
"ruff",
"ruff_diagnostics",
"ruff_formatter",
"ruff_python_ast",
"ruff_python_codegen",
"ruff_python_formatter",
"ruff_python_index",
"ruff_python_parser",
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
]
[[package]]
name = "ruff_workspace"
version = "0.0.0"
dependencies = [
"anyhow",
"colored",
"dirs 5.0.1",
"glob",
"globset",
"ignore",
"itertools",
"log",
"path-absolutize",
"pep440_rs",
"regex",
"ruff",
"ruff_cache",
"ruff_macros",
"rustc-hash",
"schemars",
"serde",
"shellexpand",
"strum",
"tempfile",
"toml",
]
[[package]]
name = "rust-stemmers"
version = "1.2.0"
@@ -2757,9 +2691,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.105"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
dependencies = [
"itoa",
"ryu",
@@ -3410,26 +3344,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.4.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
dependencies = [
"getrandom",
"rand",
"uuid-macro-internal",
"wasm-bindgen",
]
[[package]]
name = "uuid-macro-internal"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7e1ba1f333bd65ce3c9f27de592fcbc256dafe3af2717f56d7c87761fbaccf4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
]
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
[[package]]
name = "valuable"

View File

@@ -50,7 +50,6 @@ tracing = "0.1.37"
tracing-indicatif = "0.3.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
unicode-width = "0.1.10"
uuid = { version = "1.4.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
# v1.0.1

View File

@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.286
rev: v0.0.285
hooks:
- id: ruff
```

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.286"
version = "0.0.285"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""
@@ -14,16 +14,12 @@ license = { workspace = true }
[dependencies]
ruff = { path = "../ruff", default-features = false }
ruff_workspace = { path = "../ruff_workspace" }
anyhow = { workspace = true }
clap = { workspace = true }
colored = { workspace = true }
configparser = { version = "3.0.2" }
itertools = { workspace = true }
log = { workspace = true }
once_cell = { workspace = true }
pep440_rs = { version = "0.3.1", features = ["serde"] }
regex = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }
@@ -31,6 +27,3 @@ serde_json = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
toml = { workspace = true }
[dev-dependencies]
pretty_assertions = "1.3.0"

View File

@@ -1,10 +0,0 @@
use super::black::Black;
use super::isort::Isort;
use super::pep621::Project;
#[derive(Default)]
pub(crate) struct ExternalConfig<'a> {
pub(crate) black: Option<&'a Black>,
pub(crate) isort: Option<&'a Isort>,
pub(crate) project: Option<&'a Project>,
}

View File

@@ -1,24 +1,12 @@
//! Utility to generate Ruff's `pyproject.toml` section from a Flake8 INI file.
mod black;
mod converter;
mod external_config;
mod isort;
mod parser;
mod pep621;
mod plugin;
mod pyproject;
use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use configparser::ini::Ini;
use crate::converter::convert;
use crate::external_config::ExternalConfig;
use crate::plugin::Plugin;
use crate::pyproject::parse;
use ruff::flake8_to_ruff::{self, ExternalConfig};
use ruff::logging::{set_up_logging, LogLevel};
#[derive(Parser)]
@@ -37,7 +25,7 @@ struct Args {
pyproject: Option<PathBuf>,
/// List of plugins to enable.
#[arg(long, value_delimiter = ',')]
plugin: Option<Vec<Plugin>>,
plugin: Option<Vec<flake8_to_ruff::Plugin>>,
}
fn main() -> Result<()> {
@@ -51,7 +39,7 @@ fn main() -> Result<()> {
let config = ini.load(args.file).map_err(|msg| anyhow::anyhow!(msg))?;
// Read the pyproject.toml file.
let pyproject = args.pyproject.map(parse).transpose()?;
let pyproject = args.pyproject.map(flake8_to_ruff::parse).transpose()?;
let external_config = pyproject
.as_ref()
.and_then(|pyproject| pyproject.tool.as_ref())
@@ -69,7 +57,7 @@ fn main() -> Result<()> {
};
// Create Ruff's pyproject.toml section.
let pyproject = convert(&config, &external_config, args.plugin);
let pyproject = flake8_to_ruff::convert(&config, &external_config, args.plugin)?;
#[allow(clippy::print_stdout)]
{

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.286"
version = "0.0.285"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -36,9 +36,11 @@ bitflags = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["derive", "string"], optional = true }
colored = { workspace = true }
dirs = { version = "5.0.0" }
fern = { version = "0.6.1" }
glob = { workspace = true }
globset = { workspace = true }
ignore = { workspace = true }
imperative = { version = "1.0.4" }
is-macro = { workspace = true }
itertools = { workspace = true }
@@ -66,6 +68,7 @@ serde = { workspace = true }
serde_json = { workspace = true }
serde_with = { version = "3.0.0" }
similar = { workspace = true }
shellexpand = { workspace = true }
smallvec = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
@@ -74,7 +77,6 @@ toml = { workspace = true }
typed-arena = { version = "2.0.2" }
unicode-width = { workspace = true }
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
[dev-dependencies]

View File

@@ -19,12 +19,3 @@ def foo(x, y, z):
class A():
pass
# b = c
dictionary = {
# "key1": 123, # noqa: ERA001
# "key2": 456,
# "key3": 789, # test
}
#import os # noqa

View File

@@ -152,9 +152,3 @@ def f(a: Union[str, bytes, Any]) -> None: ...
def f(a: Optional[Any]) -> None: ...
def f(a: Annotated[Any, ...]) -> None: ...
def f(a: "Union[str, bytes, Any]") -> None: ...
class Foo:
@decorator()
def __init__(self: "Foo", foo: int):
...

View File

@@ -230,10 +230,6 @@ def timedelta_okay(value=dt.timedelta(hours=1)):
def path_okay(value=Path(".")):
pass
# B008 allow arbitrary call with immutable annotation
def immutable_annotation_call(value: Sequence[int] = foo()):
pass
# B006 and B008
# We should handle arbitrary nesting of these B008.
def nested_combo(a=[float(3), dt.datetime.now()]):

View File

@@ -1,18 +0,0 @@
import custom
from custom import ImmutableTypeB
def okay(foo: ImmutableTypeB = []):
...
def okay(foo: custom.ImmutableTypeA = []):
...
def okay(foo: custom.ImmutableTypeB = []):
...
def error_due_to_missing_import(foo: ImmutableTypeA = []):
...

View File

@@ -1,7 +1,6 @@
from typing import List
import fastapi
import custom
from fastapi import Query
@@ -17,9 +16,5 @@ def okay(data: List[str] = Query(None)):
...
def okay(data: custom.ImmutableTypeA = foo()):
...
def error_due_to_missing_import(data: List[str] = Depends(None)):
...

View File

@@ -15,6 +15,11 @@ filter(func, map(lambda v: v, nums))
_ = f"{set(map(lambda x: x % 2 == 0, nums))}"
_ = f"{dict(map(lambda v: (v, v**2), nums))}"
# Error, but unfixable.
# For simple expressions, this could be: `(x if x else 1 for x in nums)`.
# For more complex expressions, this would differ: `(x + 2 if x else 3 for x in nums)`.
map(lambda x=1: x, nums)
# False negatives.
map(lambda x=2, y=1: x + y, nums, nums)
set(map(lambda x, y: x, nums, nums))
@@ -32,11 +37,3 @@ map(lambda x: lambda: x, range(4))
# Error: the `x` is overridden by the inner lambda.
map(lambda x: lambda x: x, range(4))
# Ok because of the default parameters, and variadic arguments.
map(lambda x=1: x, nums)
map(lambda *args: len(args), range(4))
map(lambda **kwargs: len(kwargs), range(4))
# Ok because multiple arguments are allowed.
dict(map(lambda k, v: (k, v), keys, values))

View File

@@ -1,31 +1,22 @@
def not_checked():
import math
import math # not checked
import altair # unconventional
import matplotlib.pyplot # unconventional
import numpy # unconventional
import pandas # unconventional
import seaborn # unconventional
import tkinter # unconventional
def unconventional():
import altair
import matplotlib.pyplot
import numpy
import pandas
import seaborn
import tkinter
import networkx
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import numpy as nmp # unconventional
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import tkinter as tkr # unconventional
def unconventional_aliases():
import altair as altr
import matplotlib.pyplot as plot
import numpy as nmp
import pandas as pdas
import seaborn as sbrn
import tkinter as tkr
import networkx as nxy
def conventional_aliases():
import altair as alt
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import tkinter as tk
import networkx as nx
import altair as alt # conventional
import matplotlib.pyplot as plt # conventional
import numpy as np # conventional
import pandas as pd # conventional
import seaborn as sns # conventional
import tkinter as tk # conventional

View File

@@ -1,9 +0,0 @@
"""Test cases for difficult renames."""
def rename_global():
try:
global pandas
import pandas
except ImportError:
return False

View File

@@ -43,12 +43,3 @@ message
assert something # OK
assert something and something_else # Error
assert something and something_else and something_third # Error
def test_multiline():
assert something and something_else; x = 1
x = 1; assert something and something_else
x = 1; \
assert something and something_else

View File

@@ -68,14 +68,3 @@ import ctypes
# OK
raise ctypes.WinError(1)
# RSE102
raise IndexError()from ZeroDivisionError
raise IndexError()\
from ZeroDivisionError
raise IndexError() from ZeroDivisionError
raise IndexError();

View File

@@ -320,9 +320,3 @@ def end_of_statement():
if True:
return "" \
; # type: ignore
def end_of_file():
if False:
return 1
x = 2 \

View File

@@ -1,3 +0,0 @@
import os
import pandas
import foo.baz

View File

@@ -1,2 +0,0 @@
[tool.ruff]
line-length = 88

View File

@@ -1,37 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"import os\n",
"\n",
"math.pi"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -1,37 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"import os\n",
"\n",
"math.pi"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -28,6 +28,3 @@ mdtypes_template = {
'tag_full': [('mdtype', 'u4'), ('byte_count', 'u4')],
'tag_smalldata':[('byte_count_mdtype', 'u4'), ('data', 'S4')],
}
#: Okay
a = (1,

View File

@@ -1,13 +0,0 @@
def func():
"""\
"""
def func():
"""\\
"""
def func():
"""\ \
"""

View File

@@ -99,16 +99,3 @@ import foo.bar as bop
import foo.bar.baz
print(bop.baz.read_csv("test.csv"))
# Test: isolated deletions.
if TYPE_CHECKING:
import a1
import a2
match *0, 1, *2:
case 0,:
import b1
import b2

View File

@@ -2,5 +2,6 @@
"{bar}{}".format(1, bar=2, spam=3) # F522
"{bar:{spam}}".format(bar=2, spam=3) # No issues
"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
# Not fixable
(''
.format(x=2)) # F522
.format(x=2))

View File

@@ -28,6 +28,6 @@
"{1}{3}".format(1, 2, 3, 4) # F523, # F524
"{1} {8}".format(0, 1) # F523, # F524
# Multiline
# Not fixable
(''
.format(2))

View File

@@ -154,14 +154,3 @@ def f() -> None:
print("hello")
except A as e :
print("oh no!")
def f():
x = 1
y = 2
def f():
x = 1
y = 2

View File

@@ -15,11 +15,9 @@
"{:s} {:y}".format("hello", "world") # [bad-format-character]
"{:*^30s}".format("centered") # OK
"{:{s}}".format("hello", s="s") # OK (nested placeholder value not checked)
"{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested placeholder format spec checked)
"{0:.{prec}g}".format(1.23, prec=15) # OK (cannot validate after nested placeholder)
"{0:.{foo}{bar}{foobar}y}".format(...) # OK (cannot validate after nested placeholders)
"{0:.{foo}x{bar}y{foobar}g}".format(...) # OK (all nested placeholders are consumed without considering in between chars)
"{:{s}}".format("hello", s="s") # OK (nested replacement value not checked)
"{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked)
## f-strings

View File

@@ -1,11 +1,10 @@
class Person: # [eq-without-hash]
class Person:
def __init__(self):
self.name = "monty"
def __eq__(self, other):
return isinstance(other, Person) and other.name == self.name
# OK
class Language:
def __init__(self):
self.name = "python"
@@ -15,9 +14,3 @@ class Language:
def __hash__(self):
return hash(self.name)
class MyClass:
def __eq__(self, other):
return True
__hash__ = None

View File

@@ -1,62 +0,0 @@
import abc
class Person:
def developer_greeting(self, name): # [no-self-use]
print(f"Greetings {name}!")
def greeting_1(self): # [no-self-use]
print("Hello!")
def greeting_2(self): # [no-self-use]
print("Hi!")
# OK
def developer_greeting():
print("Greetings developer!")
# OK
class Person:
name = "Paris"
def __init__(self):
pass
def __cmp__(self, other):
print(24)
def __repr__(self):
return "Person"
def func(self):
...
def greeting_1(self):
print(f"Hello from {self.name} !")
@staticmethod
def greeting_2():
print("Hi!")
class Base(abc.ABC):
"""abstract class"""
@abstractmethod
def abstract_method(self):
"""abstract method could not be a function"""
raise NotImplementedError
class Sub(Base):
@override
def abstract_method(self):
print("concret method")
class Prop:
@property
def count(self):
return 24

View File

@@ -9,22 +9,13 @@ foo != "a" and foo != "b" and foo != "c"
foo == a or foo == "b" or foo == 3 # Mixed types.
# False negatives (the current implementation doesn't support Yoda conditions).
"a" == foo or "b" == foo or "c" == foo
"a" != foo and "b" != foo and "c" != foo
"a" == foo or foo == "b" or "c" == foo
foo == bar or baz == foo or qux == foo
foo == "a" or "b" == foo or foo == "c"
foo != "a" and "b" != foo and foo != "c"
foo == "a" or foo == "b" or "c" == bar or "d" == bar # Multiple targets
foo.bar == "a" or foo.bar == "b" # Attributes.
# OK
foo == "a" and foo == "b" and foo == "c" # `and` mixed with `==`.
@@ -45,9 +36,3 @@ foo != "a" # Single comparison.
foo == "a" == "b" or foo == "c" # Multiple comparisons.
foo == bar == "b" or foo == "c" # Multiple comparisons.
foo == foo or foo == bar # Self-comparison.
foo[0] == "a" or foo[0] == "b" # Subscripts.
foo() == "a" or foo() == "b" # Calls.

View File

@@ -1,3 +0,0 @@
from sys import *
exit(0)

View File

@@ -59,35 +59,3 @@ def f() -> None:
x = Union["str", "int"]
x: Union[str, int]
x: Union["str", "int"]
def f(x: Union[int : float]) -> None:
...
def f(x: Union[str, int : float]) -> None:
...
def f(x: Union[x := int]) -> None:
...
def f(x: Union[str, x := int]) -> None:
...
def f(x: Union[lambda: int]) -> None:
...
def f(x: Union[str, lambda: int]) -> None:
...
def f(x: Optional[int : float]) -> None:
...
def f(x: Optional[str, int : float]) -> None:
...

View File

@@ -1,7 +0,0 @@
"""
# coding=utf8""" # empty comment
"""
Invalid coding declaration since it is nested inside a docstring
The following empty comment tests for false positives as our implementation visits comments
"""

View File

@@ -1,7 +0,0 @@
# coding=utf8
print("Hello world")
"""
Regression test for https://github.com/astral-sh/ruff/issues/6756
The leading space must be removed to prevent invalid syntax.
"""

View File

@@ -1,7 +0,0 @@
# coding=utf8
print("Hello world")
"""
Regression test for https://github.com/astral-sh/ruff/issues/6756
The leading tab must be removed to prevent invalid syntax.
"""

View File

@@ -1,6 +0,0 @@
print("foo") # coding=utf8
print("Hello world")
"""
Invalid coding declaration due to a statement before the comment
"""

View File

@@ -1,7 +0,0 @@
x = 1 \
# coding=utf8
x = 2
"""
Invalid coding declaration due to continuation on preceding line
"""

View File

@@ -22,4 +22,3 @@ MyType = typing.NamedTuple("MyType", a=int, b=tuple[str, ...])
# unfixable
MyType = typing.NamedTuple("MyType", [("a", int)], [("b", str)])
MyType = typing.NamedTuple("MyType", [("a", int)], b=str)
MyType = typing.NamedTuple(typename="MyType", a=int, b=str)

View File

@@ -31,7 +31,6 @@ bool("foo")
bool("")
bool(b"")
bool(1.0)
int().denominator
# These become string or byte literals
str()
@@ -50,6 +49,3 @@ float(1.0)
bool()
bool(True)
bool(False)
# These become a literal but retain parentheses
int(1).denominator

View File

@@ -1,169 +0,0 @@
from typing import List
def func():
pass
nums = []
nums2 = []
nums3: list[int] = func()
nums4: List[int]
class C:
def append(self, x):
pass
# Errors.
# FURB113
nums.append(1)
nums.append(2)
pass
# FURB113
nums3.append(1)
nums3.append(2)
pass
# FURB113
nums4.append(1)
nums4.append(2)
pass
# FURB113
nums.append(1)
nums2.append(1)
nums.append(2)
nums.append(3)
pass
# FURB113
nums.append(1)
nums2.append(1)
nums.append(2)
# FURB113
nums3.append(1)
nums.append(3)
# FURB113
nums4.append(1)
nums4.append(2)
nums3.append(2)
pass
# FURB113
nums.append(1)
nums.append(2)
nums.append(3)
if True:
# FURB113
nums.append(1)
nums.append(2)
if True:
# FURB113
nums.append(1)
nums.append(2)
pass
if True:
# FURB113
nums.append(1)
nums2.append(1)
nums.append(2)
nums.append(3)
def yes_one(x: list[int]):
# FURB113
x.append(1)
x.append(2)
def yes_two(x: List[int]):
# FURB113
x.append(1)
x.append(2)
def yes_three(*, x: list[int]):
# FURB113
x.append(1)
x.append(2)
def yes_four(x: list[int], /):
# FURB113
x.append(1)
x.append(2)
def yes_five(x: list[int], y: list[int]):
# FURB113
x.append(1)
x.append(2)
y.append(1)
x.append(3)
def yes_six(x: list):
# FURB113
x.append(1)
x.append(2)
# Non-errors.
nums.append(1)
pass
nums.append(2)
if True:
nums.append(1)
pass
nums.append(2)
nums.append(1)
pass
nums.append(1)
nums2.append(2)
nums.copy()
nums.copy()
c = C()
c.append(1)
c.append(2)
def not_one(x):
x.append(1)
x.append(2)
def not_two(x: C):
x.append(1)
x.append(2)
# redefining a list variable with a new type shouldn't confuse ruff.
nums2 = C()
nums2.append(1)
nums2.append(2)

View File

@@ -1,64 +0,0 @@
from typing import Dict, List
names = {"key": "value"}
nums = [1, 2, 3]
x = 42
y = "hello"
# these should match
# FURB131
del nums[:]
# FURB131
del names[:]
# FURB131
del x, nums[:]
# FURB131
del y, names[:], x
def yes_one(x: list[int]):
# FURB131
del x[:]
def yes_two(x: dict[int, str]):
# FURB131
del x[:]
def yes_three(x: List[int]):
# FURB131
del x[:]
def yes_four(x: Dict[int, str]):
# FURB131
del x[:]
# these should not
del names["key"]
del nums[0]
x = 1
del x
del nums[1:2]
del nums[:2]
del nums[1:]
del nums[::2]
def no_one(param):
del param[:]

View File

@@ -1,80 +0,0 @@
from typing import Set
from some.package.name import foo, bar
s = set()
s2 = {}
s3: set[int] = foo()
# these should match
# FURB132
if "x" in s:
s.remove("x")
# FURB132
if "x" in s2:
s2.remove("x")
# FURB132
if "x" in s3:
s3.remove("x")
var = "y"
# FURB132
if var in s:
s.remove(var)
if f"{var}:{var}" in s:
s.remove(f"{var}:{var}")
def identity(x):
return x
# TODO: FURB132
if identity("x") in s2:
s2.remove(identity("x"))
# these should not
if "x" in s:
s.remove("y")
s.discard("x")
s2 = set()
if "x" in s:
s2.remove("x")
if "x" in s:
s.remove("x")
print("removed item")
if bar() in s:
# bar() might have a side effect
s.remove(bar())
if "x" in s:
s.remove("x")
else:
print("not found")
class Container:
def remove(self, item) -> None:
return
def __contains__(self, other) -> bool:
return True
c = Container()
if "x" in c:
c.remove("x")

View File

@@ -1,11 +0,0 @@
#import os # noqa
#import os # noqa: ERA001
dictionary = {
# "key1": 123, # noqa: ERA001
# "key2": 456, # noqa
# "key3": 789,
}
#import os # noqa: E501

View File

@@ -4,8 +4,8 @@ use anyhow::{bail, Result};
use libcst_native::{
Codegen, CodegenState, ImportNames, ParenthesizableWhitespace, SmallStatement, Statement,
};
use ruff_python_ast::{Ranged, Stmt};
use ruff_python_ast::Stmt;
use ruff_python_codegen::Stylist;
use ruff_source_file::Locator;
@@ -38,7 +38,7 @@ pub(crate) fn remove_imports<'a>(
locator: &Locator,
stylist: &Stylist,
) -> Result<Option<String>> {
let module_text = locator.slice(stmt);
let module_text = locator.slice(stmt.range());
let mut tree = match_statement(module_text)?;
let Statement::Simple(body) = &mut tree else {
@@ -117,7 +117,7 @@ pub(crate) fn retain_imports(
locator: &Locator,
stylist: &Stylist,
) -> Result<String> {
let module_text = locator.slice(stmt);
let module_text = locator.slice(stmt.range());
let mut tree = match_statement(module_text)?;
let Statement::Simple(body) = &mut tree else {

View File

@@ -3,14 +3,15 @@
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Stmt};
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Ranged, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::{
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
};
use ruff_source_file::{Locator, NewlineWithTrailingNewline};
use ruff_text_size::{Ranged, TextLen, TextSize};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::autofix::codemods;
@@ -47,7 +48,7 @@ pub(crate) fn delete_stmt(
} else if has_leading_content(stmt.start(), locator) {
Edit::range_deletion(stmt.range())
} else if let Some(start) = indexer.preceded_by_continuations(stmt.start(), locator) {
Edit::deletion(start, stmt.end())
Edit::range_deletion(TextRange::new(start, stmt.end()))
} else {
let range = locator.full_lines_range(stmt.range());
Edit::range_deletion(range)
@@ -132,8 +133,10 @@ pub(crate) fn remove_argument<T: Ranged>(
// Case 3: argument or keyword is the only node, so delete the arguments (but preserve
// parentheses, if needed).
Ok(match parentheses {
Parentheses::Remove => Edit::range_deletion(arguments.range()),
Parentheses::Preserve => Edit::range_replacement("()".to_string(), arguments.range()),
Parentheses::Remove => Edit::deletion(arguments.start(), arguments.end()),
Parentheses::Preserve => {
Edit::replacement("()".to_string(), arguments.start(), arguments.end())
}
})
}
}
@@ -225,25 +228,25 @@ fn trailing_semicolon(offset: TextSize, locator: &Locator) -> Option<TextSize> {
fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
let start_location = semicolon + TextSize::from(1);
for line in
NewlineWithTrailingNewline::with_offset(locator.after(start_location), start_location)
{
let contents = &locator.contents()[usize::from(start_location)..];
for line in NewlineWithTrailingNewline::from(contents) {
let trimmed = line.trim_whitespace();
// Skip past any continuations.
if trimmed.starts_with('\\') {
continue;
}
return if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
line.start()
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
line.start() + TextSize::try_from(relative_offset).unwrap()
};
return start_location
+ if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
line.start()
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
line.start() + TextSize::try_from(relative_offset).unwrap()
};
}
locator.line_end(start_location)
@@ -253,9 +256,10 @@ fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
mod tests {
use anyhow::Result;
use ruff_python_ast::Ranged;
use ruff_python_parser::parse_suite;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextSize};
use ruff_text_size::TextSize;
use crate::autofix::edits::{next_stmt_break, trailing_semicolon};

View File

@@ -1,7 +1,7 @@
use itertools::Itertools;
use std::collections::BTreeSet;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_text_size::{TextLen, TextRange, TextSize};
use rustc_hash::{FxHashMap, FxHashSet};
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel};
@@ -13,7 +13,6 @@ use crate::registry::{AsRule, Rule};
pub(crate) mod codemods;
pub(crate) mod edits;
pub(crate) mod snippet;
pub(crate) mod source_map;
pub(crate) struct FixResult {
@@ -138,9 +137,11 @@ fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Orderi
#[cfg(test)]
mod tests {
use ruff_text_size::{Ranged, TextSize};
use ruff_text_size::TextSize;
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Edit;
use ruff_diagnostics::Fix;
use ruff_source_file::Locator;
use crate::autofix::source_map::SourceMarker;

View File

@@ -1,40 +0,0 @@
use unicode_width::UnicodeWidthStr;
/// A snippet of source code for user-facing display, as in a diagnostic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SourceCodeSnippet(String);
impl SourceCodeSnippet {
pub(crate) fn new(source_code: String) -> Self {
Self(source_code)
}
pub(crate) fn from_str(source_code: &str) -> Self {
Self(source_code.to_string())
}
/// Return the full snippet for user-facing display, or `None` if the snippet should be
/// truncated.
pub(crate) fn full_display(&self) -> Option<&str> {
if Self::should_truncate(&self.0) {
None
} else {
Some(&self.0)
}
}
/// Return a truncated snippet for user-facing display.
pub(crate) fn truncated_display(&self) -> &str {
if Self::should_truncate(&self.0) {
"..."
} else {
&self.0
}
}
/// Returns `true` if the source code should be truncated when included in a user-facing
/// diagnostic.
fn should_truncate(source_code: &str) -> bool {
source_code.width() > 50 || source_code.contains(['\r', '\n'])
}
}

View File

@@ -1,4 +1,4 @@
use ruff_text_size::{Ranged, TextSize};
use ruff_text_size::TextSize;
use ruff_diagnostics::Edit;

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_text_size::Ranged;
use ruff_python_ast::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;

View File

@@ -1,7 +1,7 @@
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::Ranged;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
@@ -30,7 +30,6 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::UnusedPrivateTypedDict,
Rule::UnusedStaticMethodArgument,
Rule::UnusedVariable,
Rule::NoSelfUse,
]) {
return;
}
@@ -169,7 +168,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
continue;
}
let Some(node_id) = shadowed.source else {
let Some(statement_id) = shadowed.source else {
continue;
};
@@ -177,7 +176,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
if shadowed.kind.is_function_definition() {
if checker
.semantic
.statement(node_id)
.statement(statement_id)
.as_function_def_stmt()
.is_some_and(|function| {
visibility::is_overload(
@@ -303,12 +302,6 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pyflakes::rules::unused_import(checker, scope, &mut diagnostics);
}
}
if scope.kind.is_function() {
if checker.enabled(Rule::NoSelfUse) {
pylint::rules::no_self_use(checker, scope, &mut diagnostics);
}
}
}
checker.diagnostics.extend(diagnostics);
}

View File

@@ -1,5 +1,6 @@
use ruff_python_ast::str::raw_contents_range;
use ruff_text_size::{Ranged, TextRange};
use ruff_python_ast::Ranged;
use ruff_text_size::TextRange;
use ruff_python_semantic::{BindingKind, ContextualizedDefinition, Export};
@@ -163,9 +164,9 @@ pub(crate) fn definitions(checker: &mut Checker) {
continue;
};
let contents = checker.locator().slice(expr);
let contents = checker.locator.slice(expr.range());
let indentation = checker.locator().slice(TextRange::new(
let indentation = checker.locator.slice(TextRange::new(
checker.locator.line_start(expr.start()),
expr.start(),
));

View File

@@ -1,5 +1,4 @@
use ruff_python_ast::{self as ast, ExceptHandler};
use ruff_text_size::Ranged;
use ruff_python_ast::{self as ast, ExceptHandler, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::Rule;

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, ExprContext, Operator};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, ExprContext, Operator, Ranged};
use ruff_python_literal::cformat::{CFormatError, CFormatErrorType};
use ruff_diagnostics::Diagnostic;
@@ -6,7 +6,6 @@ use ruff_diagnostics::Diagnostic;
use ruff_python_ast::types::Node;
use ruff_python_semantic::analyze::typing;
use ruff_python_semantic::ScopeKind;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
@@ -382,34 +381,34 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Ok(summary) => {
if checker.enabled(Rule::StringDotFormatExtraNamedArguments) {
pyflakes::rules::string_dot_format_extra_named_arguments(
checker, call, &summary, keywords,
checker, &summary, keywords, location,
);
}
if checker
.enabled(Rule::StringDotFormatExtraPositionalArguments)
{
pyflakes::rules::string_dot_format_extra_positional_arguments(
checker, call, &summary, args,
checker, &summary, args, location,
);
}
if checker.enabled(Rule::StringDotFormatMissingArguments) {
pyflakes::rules::string_dot_format_missing_argument(
checker, call, &summary, args, keywords,
checker, &summary, args, keywords, location,
);
}
if checker.enabled(Rule::StringDotFormatMixingAutomatic) {
pyflakes::rules::string_dot_format_mixing_automatic(
checker, call, &summary,
checker, &summary, location,
);
}
if checker.enabled(Rule::FormatLiterals) {
pyupgrade::rules::format_literals(checker, call, &summary);
pyupgrade::rules::format_literals(checker, &summary, call);
}
if checker.enabled(Rule::FString) {
pyupgrade::rules::f_strings(
checker,
call,
&summary,
expr,
value,
checker.settings.line_length,
);
@@ -442,11 +441,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::redundant_open_modes(checker, call);
}
if checker.enabled(Rule::NativeLiterals) {
pyupgrade::rules::native_literals(
checker,
call,
checker.semantic().current_expression_parent(),
);
pyupgrade::rules::native_literals(checker, expr, func, args, keywords);
}
if checker.enabled(Rule::OpenAlias) {
pyupgrade::rules::open_alias(checker, expr, func);
@@ -1039,7 +1034,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::PrintfStringFormatting) {
pyupgrade::rules::printf_string_formatting(checker, expr, right);
pyupgrade::rules::printf_string_formatting(
checker,
expr,
right,
checker.locator,
);
}
if checker.enabled(Rule::BadStringFormatCharacter) {
pylint::rules::bad_string_format_character::percent(checker, expr);

View File

@@ -1,5 +1,4 @@
use ruff_python_ast::Parameter;
use ruff_text_size::Ranged;
use ruff_python_ast::{Parameter, Ranged};
use crate::checkers::ast::Checker;
use crate::codes::Rule;

View File

@@ -1,10 +1,11 @@
use ruff_python_ast::{self as ast, Expr, Ranged, Stmt};
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::helpers;
use ruff_python_ast::types::Node;
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::analyze::typing;
use ruff_python_semantic::ScopeKind;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
@@ -13,7 +14,7 @@ use crate::rules::{
flake8_django, flake8_errmsg, flake8_import_conventions, flake8_pie, flake8_pyi,
flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify, flake8_slots,
flake8_tidy_imports, flake8_type_checking, mccabe, pandas_vet, pep8_naming, perflint,
pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, tryceratops,
pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
};
use crate::settings::types::PythonVersion;
@@ -1056,9 +1057,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::CheckAndRemoveFromSet) {
refurb::rules::check_and_remove_from_set(checker, if_);
}
if checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::UnrecognizedVersionInfoCheck,
@@ -1462,7 +1460,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::Delete(delete @ ast::StmtDelete { targets, range: _ }) => {
Stmt::Delete(ast::StmtDelete { targets, range: _ }) => {
if checker.enabled(Rule::GlobalStatement) {
for target in targets {
if let Expr::Name(ast::ExprName { id, .. }) = target {
@@ -1470,9 +1468,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
if checker.enabled(Rule::DeleteFullSlice) {
refurb::rules::delete_full_slice(checker, delete);
}
}
Stmt::Expr(ast::StmtExpr { value, range: _ }) => {
if checker.enabled(Rule::UselessComparison) {
@@ -1490,9 +1485,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::AsyncioDanglingTask) {
ruff::rules::asyncio_dangling_task(checker, value);
}
if checker.enabled(Rule::RepeatedAppend) {
refurb::rules::repeated_append(checker, stmt);
}
}
_ => {}
}

View File

@@ -32,10 +32,10 @@ use itertools::Itertools;
use log::error;
use ruff_python_ast::{
self as ast, Arguments, Comprehension, Constant, ElifElseClause, ExceptHandler, Expr,
ExprContext, Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt,
Suite, UnaryOp,
ExprContext, Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged,
Stmt, Suite, UnaryOp,
};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, IsolationLevel};
use ruff_python_ast::all::{extract_all_names, DunderAllFlags};
@@ -52,8 +52,7 @@ use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
use ruff_python_semantic::analyze::{typing, visibility};
use ruff_python_semantic::{
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport,
SubmoduleImport,
ModuleKind, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport,
};
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_source_file::Locator;
@@ -194,6 +193,24 @@ impl<'a> Checker<'a> {
}
}
/// Returns the [`IsolationLevel`] to isolate fixes for the current statement.
///
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn statement_isolation(&self) -> IsolationLevel {
IsolationLevel::Group(self.semantic.current_statement_id().into())
}
/// Returns the [`IsolationLevel`] to isolate fixes in the current statement's parent.
pub(crate) fn parent_isolation(&self) -> IsolationLevel {
self.semantic
.current_statement_parent_id()
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default()
}
/// The [`Locator`] for the current file, which enables extraction of source code from byte
/// offsets.
pub(crate) const fn locator(&self) -> &'a Locator<'a> {
@@ -242,18 +259,6 @@ impl<'a> Checker<'a> {
pub(crate) const fn any_enabled(&self, rules: &[Rule]) -> bool {
self.settings.rules.any_enabled(rules)
}
/// Returns the [`IsolationLevel`] to isolate fixes for a given node.
///
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn isolation(node_id: Option<NodeId>) -> IsolationLevel {
node_id
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default()
}
}
impl<'a, 'b> Visitor<'b> for Checker<'a>
@@ -262,7 +267,7 @@ where
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
// Step 0: Pre-processing
self.semantic.push_node(stmt);
self.semantic.push_statement(stmt);
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
@@ -774,7 +779,7 @@ where
analyze::statement(stmt, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_node();
self.semantic.pop_statement();
}
fn visit_annotation(&mut self, expr: &'b Expr) {
@@ -810,7 +815,7 @@ where
return;
}
self.semantic.push_node(expr);
self.semantic.push_expression(expr);
// Store the flags prior to any further descent, so that we can restore them after visiting
// the node.
@@ -1230,7 +1235,7 @@ where
analyze::expression(expr, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_node();
self.semantic.pop_expression();
}
fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) {
@@ -1895,8 +1900,6 @@ impl<'a> Checker<'a> {
for (name, range) in exports {
if let Some(binding_id) = self.semantic.global_scope().get(name) {
// Mark anything referenced in `__all__` as used.
// TODO(charlie): `range` here should be the range of the name in `__all__`, not
// the range of `__all__` itself.
self.semantic.add_global_reference(binding_id, range);
} else {
if self.semantic.global_scope().uses_star_imports() {

View File

@@ -2,15 +2,16 @@
use std::borrow::Cow;
use std::path::Path;
use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt, Suite};
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::helpers::to_module_path;
use ruff_python_ast::imports::{ImportMap, ModuleImport};
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::{self as ast, PySourceType, Stmt, Suite};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
use crate::directives::IsortDirectives;
use crate::registry::Rule;

View File

@@ -1,9 +1,10 @@
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use ruff_python_ast::Ranged;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::TokenKind;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::TextRange;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle::rules::logical_lines::{

View File

@@ -3,7 +3,8 @@
use std::path::Path;
use itertools::Itertools;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_python_ast::Ranged;
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_source_file::Locator;
@@ -109,8 +110,8 @@ pub(crate) fn check_noqa(
let mut diagnostic =
Diagnostic::new(UnusedNOQA { codes: None }, directive.range());
if settings.rules.should_fix(diagnostic.kind.rule()) {
diagnostic
.set_fix(Fix::suggested(delete_noqa(directive.range(), locator)));
#[allow(deprecated)]
diagnostic.set_fix_from_edit(delete_noqa(directive.range(), locator));
}
diagnostics.push(diagnostic);
}
@@ -174,12 +175,12 @@ pub(crate) fn check_noqa(
);
if settings.rules.should_fix(diagnostic.kind.rule()) {
if valid_codes.is_empty() {
diagnostic.set_fix(Fix::suggested(delete_noqa(
directive.range(),
locator,
)));
#[allow(deprecated)]
diagnostic
.set_fix_from_edit(delete_noqa(directive.range(), locator));
} else {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
format!("# noqa: {}", valid_codes.join(", ")),
directive.range(),
)));

View File

@@ -1,9 +1,10 @@
//! Lint rules based on checking physical lines.
use ruff_text_size::TextSize;
use ruff_diagnostics::Diagnostic;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_source_file::{Locator, UniversalNewlines};
use ruff_text_size::TextSize;
use crate::registry::Rule;
use crate::rules::flake8_copyright::rules::missing_copyright_notice;
@@ -98,10 +99,11 @@ pub(crate) fn check_physical_lines(
#[cfg(test)]
mod tests {
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::lex;
use ruff_python_parser::Mode;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use crate::line_width::LineLength;
@@ -130,7 +132,7 @@ mod tests {
},
)
};
let line_length = LineLength::try_from(8).unwrap();
let line_length = LineLength::from(8);
assert_eq!(check_with_max_line_length(line_length), vec![]);
assert_eq!(check_with_max_line_length(line_length), vec![]);
}

View File

@@ -216,7 +216,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R1722") => (RuleGroup::Unspecified, rules::pylint::rules::SysExitAlias),
(Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf),
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
(Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop),
(Pylint, "W0127") => (RuleGroup::Unspecified, rules::pylint::rules::SelfAssigningVariable),
(Pylint, "W0129") => (RuleGroup::Unspecified, rules::pylint::rules::AssertOnStringLiteral),
@@ -865,11 +864,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Slots, "001") => (RuleGroup::Unspecified, rules::flake8_slots::rules::NoSlotsInTupleSubclass),
(Flake8Slots, "002") => (RuleGroup::Unspecified, rules::flake8_slots::rules::NoSlotsInNamedtupleSubclass),
// refurb
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
_ => return None,
})
}

View File

@@ -1,11 +1,9 @@
use crate::autofix::codemods::CodegenStylist;
use anyhow::{bail, Result};
use libcst_native::{
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FunctionDef,
GeneratorExp, If, Import, ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda,
ListComp, Module, Name, SmallStatement, Statement, Suite, Tuple, With,
};
use ruff_python_codegen::Stylist;
pub(crate) fn match_module(module_text: &str) -> Result<Module> {
match libcst_native::parse_module(module_text, None) {
@@ -14,6 +12,13 @@ pub(crate) fn match_module(module_text: &str) -> Result<Module> {
}
}
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
match libcst_native::parse_expression(expression_text) {
Ok(expression) => Ok(expression),
Err(_) => bail!("Failed to extract expression from source"),
}
}
pub(crate) fn match_statement(statement_text: &str) -> Result<Statement> {
match libcst_native::parse_statement(statement_text) {
Ok(statement) => Ok(statement),
@@ -200,59 +205,3 @@ pub(crate) fn match_if<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a m
bail!("Expected Statement::Compound")
}
}
/// Given the source code for an expression, return the parsed [`Expression`].
///
/// If the expression is not guaranteed to be valid as a standalone expression (e.g., if it may
/// span multiple lines and/or require parentheses), use [`transform_expression`] instead.
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
match libcst_native::parse_expression(expression_text) {
Ok(expression) => Ok(expression),
Err(_) => bail!("Failed to extract expression from source"),
}
}
/// Run a transformation function over an expression.
///
/// Passing an expression to [`match_expression`] directly can lead to parse errors if the
/// expression is not a valid standalone expression (e.g., it was parenthesized in the original
/// source). This method instead wraps the expression in "fake" parentheses, runs the
/// transformation, then removes the "fake" parentheses.
pub(crate) fn transform_expression(
source_code: &str,
stylist: &Stylist,
func: impl FnOnce(Expression) -> Result<Expression>,
) -> Result<String> {
// Wrap the expression in parentheses.
let source_code = format!("({source_code})");
let expression = match_expression(&source_code)?;
// Run the function on the expression.
let expression = func(expression)?;
// Codegen the expression.
let mut source_code = expression.codegen_stylist(stylist);
// Drop the outer parentheses.
source_code.drain(0..1);
source_code.drain(source_code.len() - 1..source_code.len());
Ok(source_code)
}
/// Like [`transform_expression`], but operates on the source code of the expression, rather than
/// the parsed [`Expression`]. This _shouldn't_ exist, but does to accommodate lifetime issues.
pub(crate) fn transform_expression_text(
source_code: &str,
func: impl FnOnce(String) -> Result<String>,
) -> Result<String> {
// Wrap the expression in parentheses.
let source_code = format!("({source_code})");
// Run the function on the expression.
let mut transformed = func(source_code)?;
// Drop the outer parentheses.
transformed.drain(0..1);
transformed.drain(transformed.len() - 1..transformed.len());
Ok(transformed)
}

View File

@@ -5,7 +5,7 @@ use std::str::FromStr;
use bitflags::bitflags;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_python_index::Indexer;
use ruff_source_file::Locator;

View File

@@ -3,10 +3,10 @@
use std::iter::FusedIterator;
use ruff_python_ast::{self as ast, Constant, Expr, Stmt, Suite};
use ruff_python_ast::{self as ast, Constant, Expr, Ranged, Stmt, Suite};
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_text_size::{Ranged, TextSize};
use ruff_text_size::TextSize;
use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor};
use ruff_source_file::{Locator, UniversalNewlineIterator};
@@ -76,7 +76,7 @@ impl StatementVisitor<'_> for StringLinesVisitor<'_> {
}) = value.as_ref()
{
for line in UniversalNewlineIterator::with_offset(
self.locator.slice(value.as_ref()),
self.locator.slice(value.range()),
value.start(),
) {
self.string_lines.push(line.start());

View File

@@ -1,9 +1,9 @@
use std::fmt::{Debug, Formatter};
use std::ops::Deref;
use ruff_python_ast::Expr;
use ruff_python_ast::{Expr, Ranged};
use ruff_python_semantic::Definition;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::TextRange;
pub(crate) mod extraction;
pub(crate) mod google;

View File

@@ -2,7 +2,8 @@ use std::fmt::{Debug, Formatter};
use std::iter::FusedIterator;
use ruff_python_ast::docstrings::{leading_space, leading_words};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_python_ast::Ranged;
use ruff_text_size::{TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
use ruff_source_file::{Line, UniversalNewlineIterator, UniversalNewlines};
@@ -164,7 +165,7 @@ impl<'a> SectionContexts<'a> {
lines.peek(),
) {
if let Some(mut last) = last.take() {
last.range = TextRange::new(last.start(), line.start());
last.range = TextRange::new(last.range.start(), line.start());
contexts.push(last);
}
@@ -181,7 +182,7 @@ impl<'a> SectionContexts<'a> {
}
if let Some(mut last) = last.take() {
last.range = TextRange::new(last.start(), contents.text_len());
last.range = TextRange::new(last.range.start(), contents.text_len());
contexts.push(last);
}
@@ -267,12 +268,6 @@ struct SectionContextData {
summary_full_end: TextSize,
}
impl Ranged for SectionContextData {
fn range(&self) -> TextRange {
self.range
}
}
pub(crate) struct SectionContext<'a> {
data: &'a SectionContextData,
docstring_body: DocstringBody<'a>,

View File

@@ -1,13 +1,13 @@
//! Extract Black configuration settings from a pyproject.toml.
use ruff::line_width::LineLength;
use ruff::settings::types::PythonVersion;
use serde::{Deserialize, Serialize};
use crate::settings::types::PythonVersion;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub(crate) struct Black {
pub struct Black {
#[serde(alias = "line-length", alias = "line_length")]
pub(crate) line_length: Option<LineLength>,
pub line_length: Option<usize>,
#[serde(alias = "target-version", alias = "target_version")]
pub(crate) target_version: Option<Vec<PythonVersion>>,
pub target_version: Option<Vec<PythonVersion>>,
}

View File

@@ -1,25 +1,25 @@
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use anyhow::Result;
use itertools::Itertools;
use ruff::line_width::LineLength;
use ruff::registry::Linter;
use ruff::rule_selector::RuleSelector;
use ruff::rules::flake8_pytest_style::types::{
use crate::line_width::LineLength;
use crate::registry::Linter;
use crate::rule_selector::RuleSelector;
use crate::rules::flake8_pytest_style::types::{
ParametrizeNameType, ParametrizeValuesRowType, ParametrizeValuesType,
};
use ruff::rules::flake8_quotes::settings::Quote;
use ruff::rules::flake8_tidy_imports::settings::Strictness;
use ruff::rules::pydocstyle::settings::Convention;
use ruff::settings::types::PythonVersion;
use ruff::warn_user;
use ruff_workspace::options::{
Flake8AnnotationsOptions, Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ErrMsgOptions,
Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, McCabeOptions,
Options, Pep8NamingOptions, PydocstyleOptions,
use crate::rules::flake8_quotes::settings::Quote;
use crate::rules::flake8_tidy_imports::settings::Strictness;
use crate::rules::pydocstyle::settings::Convention;
use crate::rules::{
flake8_annotations, flake8_bugbear, flake8_builtins, flake8_errmsg, flake8_pytest_style,
flake8_quotes, flake8_tidy_imports, mccabe, pep8_naming, pydocstyle,
};
use ruff_workspace::pyproject::Pyproject;
use crate::settings::options::Options;
use crate::settings::pyproject::Pyproject;
use crate::settings::types::PythonVersion;
use crate::warn_user;
use super::external_config::ExternalConfig;
use super::plugin::Plugin;
@@ -30,11 +30,11 @@ const DEFAULT_SELECTORS: &[RuleSelector] = &[
RuleSelector::Linter(Linter::Pycodestyle),
];
pub(crate) fn convert(
pub fn convert(
config: &HashMap<String, HashMap<String, Option<String>>>,
external_config: &ExternalConfig,
plugins: Option<Vec<Plugin>>,
) -> Pyproject {
) -> Result<Pyproject> {
// Extract the Flake8 section.
let flake8 = config
.get("flake8")
@@ -103,16 +103,16 @@ pub(crate) fn convert(
// Parse each supported option.
let mut options = Options::default();
let mut flake8_annotations = Flake8AnnotationsOptions::default();
let mut flake8_bugbear = Flake8BugbearOptions::default();
let mut flake8_builtins = Flake8BuiltinsOptions::default();
let mut flake8_errmsg = Flake8ErrMsgOptions::default();
let mut flake8_pytest_style = Flake8PytestStyleOptions::default();
let mut flake8_quotes = Flake8QuotesOptions::default();
let mut flake8_tidy_imports = Flake8TidyImportsOptions::default();
let mut mccabe = McCabeOptions::default();
let mut pep8_naming = Pep8NamingOptions::default();
let mut pydocstyle = PydocstyleOptions::default();
let mut flake8_annotations = flake8_annotations::settings::Options::default();
let mut flake8_bugbear = flake8_bugbear::settings::Options::default();
let mut flake8_builtins = flake8_builtins::settings::Options::default();
let mut flake8_errmsg = flake8_errmsg::settings::Options::default();
let mut flake8_pytest_style = flake8_pytest_style::settings::Options::default();
let mut flake8_quotes = flake8_quotes::settings::Options::default();
let mut flake8_tidy_imports = flake8_tidy_imports::options::Options::default();
let mut mccabe = mccabe::settings::Options::default();
let mut pep8_naming = pep8_naming::settings::Options::default();
let mut pydocstyle = pydocstyle::settings::Options::default();
for (key, value) in flake8 {
if let Some(value) = value {
match key.as_str() {
@@ -120,8 +120,10 @@ pub(crate) fn convert(
"builtins" => {
options.builtins = Some(parser::parse_strings(value.as_ref()));
}
"max-line-length" | "max_line_length" => match LineLength::from_str(value) {
Ok(line_length) => options.line_length = Some(line_length),
"max-line-length" | "max_line_length" => match value.parse::<usize>() {
Ok(line_length) => {
options.line_length = Some(LineLength::from(line_length));
}
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
@@ -370,41 +372,41 @@ pub(crate) fn convert(
.sorted_by_key(RuleSelector::prefix_and_code)
.collect(),
);
if flake8_annotations != Flake8AnnotationsOptions::default() {
if flake8_annotations != flake8_annotations::settings::Options::default() {
options.flake8_annotations = Some(flake8_annotations);
}
if flake8_bugbear != Flake8BugbearOptions::default() {
if flake8_bugbear != flake8_bugbear::settings::Options::default() {
options.flake8_bugbear = Some(flake8_bugbear);
}
if flake8_builtins != Flake8BuiltinsOptions::default() {
if flake8_builtins != flake8_builtins::settings::Options::default() {
options.flake8_builtins = Some(flake8_builtins);
}
if flake8_errmsg != Flake8ErrMsgOptions::default() {
if flake8_errmsg != flake8_errmsg::settings::Options::default() {
options.flake8_errmsg = Some(flake8_errmsg);
}
if flake8_pytest_style != Flake8PytestStyleOptions::default() {
if flake8_pytest_style != flake8_pytest_style::settings::Options::default() {
options.flake8_pytest_style = Some(flake8_pytest_style);
}
if flake8_quotes != Flake8QuotesOptions::default() {
if flake8_quotes != flake8_quotes::settings::Options::default() {
options.flake8_quotes = Some(flake8_quotes);
}
if flake8_tidy_imports != Flake8TidyImportsOptions::default() {
if flake8_tidy_imports != flake8_tidy_imports::options::Options::default() {
options.flake8_tidy_imports = Some(flake8_tidy_imports);
}
if mccabe != McCabeOptions::default() {
if mccabe != mccabe::settings::Options::default() {
options.mccabe = Some(mccabe);
}
if pep8_naming != Pep8NamingOptions::default() {
if pep8_naming != pep8_naming::settings::Options::default() {
options.pep8_naming = Some(pep8_naming);
}
if pydocstyle != PydocstyleOptions::default() {
if pydocstyle != pydocstyle::settings::Options::default() {
options.pydocstyle = Some(pydocstyle);
}
// Extract any settings from the existing `pyproject.toml`.
if let Some(black) = &external_config.black {
if let Some(line_length) = &black.line_length {
options.line_length = Some(*line_length);
options.line_length = Some(LineLength::from(*line_length));
}
if let Some(target_version) = &black.target_version {
@@ -437,7 +439,7 @@ pub(crate) fn convert(
}
// Create the pyproject.toml.
Pyproject::new(options)
Ok(Pyproject::new(options))
}
/// Resolve the set of enabled `RuleSelector` values for the given
@@ -456,20 +458,19 @@ mod tests {
use anyhow::Result;
use itertools::Itertools;
use pep440_rs::VersionSpecifiers;
use pretty_assertions::assert_eq;
use ruff::line_width::LineLength;
use ruff::registry::Linter;
use ruff::rule_selector::RuleSelector;
use ruff::rules::flake8_quotes;
use ruff::rules::pydocstyle::settings::Convention;
use ruff::settings::types::PythonVersion;
use ruff_workspace::options::{Flake8QuotesOptions, Options, PydocstyleOptions};
use ruff_workspace::pyproject::Pyproject;
use crate::converter::DEFAULT_SELECTORS;
use crate::pep621::Project;
use crate::ExternalConfig;
use crate::flake8_to_ruff::converter::DEFAULT_SELECTORS;
use crate::flake8_to_ruff::pep621::Project;
use crate::flake8_to_ruff::ExternalConfig;
use crate::line_width::LineLength;
use crate::registry::Linter;
use crate::rule_selector::RuleSelector;
use crate::rules::pydocstyle::settings::Convention;
use crate::rules::{flake8_quotes, pydocstyle};
use crate::settings::options::Options;
use crate::settings::pyproject::Pyproject;
use crate::settings::types::PythonVersion;
use super::super::plugin::Plugin;
use super::convert;
@@ -490,18 +491,20 @@ mod tests {
}
#[test]
fn it_converts_empty() {
fn it_converts_empty() -> Result<()> {
let actual = convert(
&HashMap::from([("flake8".to_string(), HashMap::default())]),
&ExternalConfig::default(),
None,
);
)?;
let expected = Pyproject::new(default_options([]));
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn it_converts_dashes() {
fn it_converts_dashes() -> Result<()> {
let actual = convert(
&HashMap::from([(
"flake8".to_string(),
@@ -509,16 +512,18 @@ mod tests {
)]),
&ExternalConfig::default(),
Some(vec![]),
);
)?;
let expected = Pyproject::new(Options {
line_length: Some(LineLength::try_from(100).unwrap()),
line_length: Some(LineLength::from(100)),
..default_options([])
});
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn it_converts_underscores() {
fn it_converts_underscores() -> Result<()> {
let actual = convert(
&HashMap::from([(
"flake8".to_string(),
@@ -526,16 +531,18 @@ mod tests {
)]),
&ExternalConfig::default(),
Some(vec![]),
);
)?;
let expected = Pyproject::new(Options {
line_length: Some(LineLength::try_from(100).unwrap()),
line_length: Some(LineLength::from(100)),
..default_options([])
});
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn it_ignores_parse_errors() {
fn it_ignores_parse_errors() -> Result<()> {
let actual = convert(
&HashMap::from([(
"flake8".to_string(),
@@ -543,13 +550,15 @@ mod tests {
)]),
&ExternalConfig::default(),
Some(vec![]),
);
)?;
let expected = Pyproject::new(default_options([]));
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn it_converts_plugin_options() {
fn it_converts_plugin_options() -> Result<()> {
let actual = convert(
&HashMap::from([(
"flake8".to_string(),
@@ -557,9 +566,9 @@ mod tests {
)]),
&ExternalConfig::default(),
Some(vec![]),
);
)?;
let expected = Pyproject::new(Options {
flake8_quotes: Some(Flake8QuotesOptions {
flake8_quotes: Some(flake8_quotes::settings::Options {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
@@ -568,10 +577,12 @@ mod tests {
..default_options([])
});
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn it_converts_docstring_conventions() {
fn it_converts_docstring_conventions() -> Result<()> {
let actual = convert(
&HashMap::from([(
"flake8".to_string(),
@@ -582,9 +593,9 @@ mod tests {
)]),
&ExternalConfig::default(),
Some(vec![Plugin::Flake8Docstrings]),
);
)?;
let expected = Pyproject::new(Options {
pydocstyle: Some(PydocstyleOptions {
pydocstyle: Some(pydocstyle::settings::Options {
convention: Some(Convention::Numpy),
ignore_decorators: None,
property_decorators: None,
@@ -592,10 +603,12 @@ mod tests {
..default_options([Linter::Pydocstyle.into()])
});
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn it_infers_plugins_if_omitted() {
fn it_infers_plugins_if_omitted() -> Result<()> {
let actual = convert(
&HashMap::from([(
"flake8".to_string(),
@@ -603,9 +616,9 @@ mod tests {
)]),
&ExternalConfig::default(),
None,
);
)?;
let expected = Pyproject::new(Options {
flake8_quotes: Some(Flake8QuotesOptions {
flake8_quotes: Some(flake8_quotes::settings::Options {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
@@ -614,6 +627,8 @@ mod tests {
..default_options([Linter::Flake8Quotes.into()])
});
assert_eq!(actual, expected);
Ok(())
}
#[test]
@@ -627,7 +642,7 @@ mod tests {
..ExternalConfig::default()
},
Some(vec![]),
);
)?;
let expected = Pyproject::new(Options {
target_version: Some(PythonVersion::Py38),
..default_options([])

View File

@@ -0,0 +1,10 @@
use super::black::Black;
use super::isort::Isort;
use super::pep621::Project;
#[derive(Default)]
pub struct ExternalConfig<'a> {
pub black: Option<&'a Black>,
pub isort: Option<&'a Isort>,
pub project: Option<&'a Project>,
}

View File

@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
/// The [isort configuration](https://pycqa.github.io/isort/docs/configuration/config_files.html).
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub(crate) struct Isort {
pub struct Isort {
#[serde(alias = "src-paths", alias = "src_paths")]
pub(crate) src_paths: Option<Vec<String>>,
pub src_paths: Option<Vec<String>>,
}

View File

@@ -0,0 +1,13 @@
pub use converter::convert;
pub use external_config::ExternalConfig;
pub use plugin::Plugin;
pub use pyproject::parse;
mod black;
mod converter;
mod external_config;
mod isort;
mod parser;
pub mod pep621;
mod plugin;
mod pyproject;

View File

@@ -3,10 +3,12 @@ use std::str::FromStr;
use anyhow::{bail, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use ruff::settings::types::PatternPrefixPair;
use ruff::{warn_user, RuleSelector};
use rustc_hash::FxHashMap;
use crate::rule_selector::RuleSelector;
use crate::settings::types::PatternPrefixPair;
use crate::warn_user;
static COMMA_SEPARATED_LIST_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").unwrap());
/// Parse a comma-separated list of `RuleSelector` values (e.g.,
@@ -192,11 +194,11 @@ pub(crate) fn collect_per_file_ignores(
#[cfg(test)]
mod tests {
use anyhow::Result;
use ruff::RuleSelector;
use ruff::codes;
use ruff::registry::Linter;
use ruff::settings::types::PatternPrefixPair;
use crate::codes;
use crate::registry::Linter;
use crate::rule_selector::RuleSelector;
use crate::settings::types::PatternPrefixPair;
use super::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings};

View File

@@ -4,7 +4,7 @@ use pep440_rs::VersionSpecifiers;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub(crate) struct Project {
pub struct Project {
#[serde(alias = "requires-python", alias = "requires_python")]
pub(crate) requires_python: Option<VersionSpecifiers>,
pub requires_python: Option<VersionSpecifiers>,
}

View File

@@ -3,8 +3,9 @@ use std::fmt;
use std::str::FromStr;
use anyhow::anyhow;
use ruff::registry::Linter;
use ruff::RuleSelector;
use crate::registry::Linter;
use crate::rule_selector::RuleSelector;
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum Plugin {

View File

@@ -8,18 +8,18 @@ use super::isort::Isort;
use super::pep621::Project;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Tools {
pub(crate) black: Option<Black>,
pub(crate) isort: Option<Isort>,
pub struct Tools {
pub black: Option<Black>,
pub isort: Option<Isort>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Pyproject {
pub(crate) tool: Option<Tools>,
pub(crate) project: Option<Project>,
pub struct Pyproject {
pub tool: Option<Tools>,
pub project: Option<Project>,
}
pub(crate) fn parse<P: AsRef<Path>>(path: P) -> Result<Pyproject> {
pub fn parse<P: AsRef<Path>>(path: P) -> Result<Pyproject> {
let contents = std::fs::read_to_string(path)?;
let pyproject = toml::from_str::<Pyproject>(&contents)?;
Ok(pyproject)

View File

@@ -1,9 +1,9 @@
//! Insert statements into Python code.
use std::ops::Add;
use ruff_python_ast::{PySourceType, Stmt};
use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_text_size::{Ranged, TextSize};
use ruff_text_size::TextSize;
use ruff_diagnostics::Edit;
use ruff_python_ast::helpers::is_docstring_stmt;

View File

@@ -7,8 +7,8 @@ use std::error::Error;
use anyhow::Result;
use libcst_native::{ImportAlias, Name, NameOrAttribute};
use ruff_python_ast::{self as ast, PySourceType, Stmt, Suite};
use ruff_text_size::{Ranged, TextSize};
use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt, Suite};
use ruff_text_size::TextSize;
use ruff_diagnostics::Edit;
use ruff_python_ast::imports::{AnyImport, Import, ImportFrom};
@@ -301,14 +301,12 @@ impl<'a> Importer<'a> {
}
if let Stmt::ImportFrom(ast::StmtImportFrom {
module: name,
names,
level,
range: _,
..
}) = stmt
{
if level.map_or(true, |level| level.to_u32() == 0)
&& name.as_ref().is_some_and(|name| name == module)
&& names.iter().all(|alias| alias.name.as_str() != "*")
{
import_from = Some(*stmt);
}
@@ -319,7 +317,7 @@ impl<'a> Importer<'a> {
/// Add the given member to an existing `Stmt::ImportFrom` statement.
fn add_member(&self, stmt: &Stmt, member: &str) -> Result<Edit> {
let mut statement = match_statement(self.locator.slice(stmt))?;
let mut statement = match_statement(self.locator.slice(stmt.range()))?;
let import_from = match_import_from(&mut statement)?;
let aliases = match_aliases(import_from)?;
aliases.push(ImportAlias {

View File

@@ -3,14 +3,14 @@
/// When we lint a jupyter notebook, we have to translate the row/column based on
/// [`ruff_text_size::TextSize`] to jupyter notebook cell/row/column.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NotebookIndex {
pub struct JupyterIndex {
/// Enter a row (1-based), get back the cell (1-based)
pub(super) row_to_cell: Vec<u32>,
/// Enter a row (1-based), get back the row in cell (1-based)
pub(super) row_to_row_in_cell: Vec<u32>,
}
impl NotebookIndex {
impl JupyterIndex {
/// Returns the cell number (1-based) for the given row (1-based).
pub fn cell(&self, row: usize) -> Option<u32> {
self.row_to_cell.get(row).copied()

View File

@@ -9,7 +9,6 @@ use itertools::Itertools;
use once_cell::sync::OnceCell;
use serde::Serialize;
use serde_json::error::Category;
use uuid::Uuid;
use ruff_diagnostics::Diagnostic;
use ruff_python_parser::lexer::lex;
@@ -18,7 +17,7 @@ use ruff_source_file::{NewlineWithTrailingNewline, UniversalNewlineIterator};
use ruff_text_size::{TextRange, TextSize};
use crate::autofix::source_map::{SourceMap, SourceMarker};
use crate::jupyter::index::NotebookIndex;
use crate::jupyter::index::JupyterIndex;
use crate::jupyter::schema::{Cell, RawNotebook, SortAlphabetically, SourceValue};
use crate::rules::pycodestyle::rules::SyntaxError;
use crate::IOError;
@@ -83,8 +82,8 @@ impl Cell {
Cell::Code(cell) => &cell.source,
_ => return false,
};
// Ignore cells containing cell magic as they act on the entire cell
// as compared to line magic which acts on a single line.
// Ignore cells containing cell magic. This is different from line magic
// which is allowed and ignored by the parser.
!match source {
SourceValue::String(string) => string
.lines()
@@ -107,7 +106,7 @@ pub struct Notebook {
source_code: String,
/// The index of the notebook. This is used to map between the concatenated
/// source code and the original notebook.
index: OnceCell<NotebookIndex>,
index: OnceCell<JupyterIndex>,
/// The raw notebook i.e., the deserialized version of JSON string.
raw: RawNotebook,
/// The offsets of each cell in the concatenated source code. This includes
@@ -157,7 +156,7 @@ impl Notebook {
TextRange::default(),
)
})?;
let mut raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
let raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
Ok(notebook) => notebook,
Err(err) => {
// Translate the error into a diagnostic
@@ -263,23 +262,6 @@ impl Notebook {
cell_offsets.push(current_offset);
}
// Add cell ids to 4.5+ notebooks if they are missing
// https://github.com/astral-sh/ruff/issues/6834
// https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md#required-field
if raw_notebook.nbformat == 4 && raw_notebook.nbformat_minor >= 5 {
for cell in &mut raw_notebook.cells {
let id = match cell {
Cell::Code(cell) => &mut cell.id,
Cell::Markdown(cell) => &mut cell.id,
Cell::Raw(cell) => &mut cell.id,
};
if id.is_none() {
// https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md#questions
*id = Some(Uuid::new_v4().to_string());
}
}
}
Ok(Self {
raw: raw_notebook,
index: OnceCell::new(),
@@ -386,7 +368,7 @@ impl Notebook {
///
/// The index building is expensive as it needs to go through the content of
/// every valid code cell.
fn build_index(&self) -> NotebookIndex {
fn build_index(&self) -> JupyterIndex {
let mut row_to_cell = vec![0];
let mut row_to_row_in_cell = vec![0];
@@ -413,7 +395,7 @@ impl Notebook {
row_to_row_in_cell.extend(1..=line_count);
}
NotebookIndex {
JupyterIndex {
row_to_cell,
row_to_row_in_cell,
}
@@ -431,7 +413,7 @@ impl Notebook {
/// The index is built only once when required. This is only used to
/// report diagnostics, so by that time all of the autofixes must have
/// been applied if `--fix` was passed.
pub(crate) fn index(&self) -> &NotebookIndex {
pub(crate) fn index(&self) -> &JupyterIndex {
self.index.get_or_init(|| self.build_index())
}
@@ -491,14 +473,12 @@ mod tests {
use anyhow::Result;
use test_case::test_case;
use crate::jupyter::index::NotebookIndex;
use crate::jupyter::index::JupyterIndex;
use crate::jupyter::schema::Cell;
use crate::jupyter::Notebook;
use crate::registry::Rule;
use crate::source_kind::SourceKind;
use crate::test::{
read_jupyter_notebook, test_contents, test_notebook_path, test_resource_path,
TestedNotebook,
read_jupyter_notebook, test_notebook_path, test_resource_path, TestedNotebook,
};
use crate::{assert_messages, settings};
@@ -579,7 +559,7 @@ print("after empty cells")
);
assert_eq!(
notebook.index(),
&NotebookIndex {
&JupyterIndex {
row_to_cell: vec![0, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 5, 7, 7, 8],
row_to_row_in_cell: vec![0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 1, 1, 2, 1],
}
@@ -679,28 +659,4 @@ print("after empty cells")
Ok(())
}
// Version <4.5, don't emit cell ids
#[test_case(Path::new("no_cell_id.ipynb"), false; "no_cell_id")]
// Version 4.5, cell ids are missing and need to be added
#[test_case(Path::new("add_missing_cell_id.ipynb"), true; "add_missing_cell_id")]
fn test_cell_id(path: &Path, has_id: bool) -> Result<()> {
let source_notebook = read_jupyter_notebook(path)?;
let source_kind = SourceKind::IpyNotebook(source_notebook);
let (_, transformed) = test_contents(
&source_kind,
path,
&settings::Settings::for_rule(Rule::UnusedImport),
);
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
let mut writer = Vec::new();
linted_notebook.write_inner(&mut writer)?;
let actual = String::from_utf8(writer)?;
if has_id {
assert!(actual.contains(r#""id": ""#));
} else {
assert!(!actual.contains(r#""id":"#));
}
Ok(())
}
}

View File

@@ -150,7 +150,6 @@ pub struct CodeCell {
/// Technically, id isn't required (it's not even present) in schema v4.0 through v4.4, but
/// it's required in v4.5. Main issue is that pycharm creates notebooks without an id
/// <https://youtrack.jetbrains.com/issue/PY-59438/Jupyter-notebooks-created-with-PyCharm-are-missing-the-id-field-in-cells-in-the-.ipynb-json>
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// Cell-level metadata.
pub metadata: Value,

View File

@@ -12,12 +12,13 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
mod autofix;
mod checkers;
pub mod codes;
mod codes;
mod comments;
mod cst;
pub mod directives;
mod doc_lines;
mod docstrings;
pub mod flake8_to_ruff;
pub mod fs;
mod importer;
pub mod jupyter;
@@ -31,8 +32,9 @@ pub mod packaging;
pub mod pyproject_toml;
pub mod registry;
mod renamer;
pub mod resolver;
mod rule_redirects;
pub mod rule_selector;
mod rule_selector;
pub mod rules;
pub mod settings;
pub mod source_kind;
@@ -40,5 +42,3 @@ pub mod upstream_categories;
#[cfg(any(test, fuzzing))]
pub mod test;
pub const RUFF_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");

View File

@@ -1,111 +1,30 @@
use ruff_cache::{CacheKey, CacheKeyHasher};
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::hash::Hasher;
use std::num::{NonZeroU16, NonZeroU8, ParseIntError};
use std::str::FromStr;
use std::num::NonZeroU8;
use unicode_width::UnicodeWidthChar;
use ruff_macros::CacheKey;
/// The length of a line of text that is considered too long.
///
/// The allowed range of values is 1..=320
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, CacheKey)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct LineLength(NonZeroU16);
impl LineLength {
/// Maximum allowed value for a valid [`LineLength`]
pub const MAX: u16 = 320;
/// Return the numeric value for this [`LineLength`]
pub fn value(&self) -> u16 {
self.0.get()
}
}
pub struct LineLength(usize);
impl Default for LineLength {
/// The default line length.
fn default() -> Self {
Self(NonZeroU16::new(88).unwrap())
Self(88)
}
}
impl CacheKey for LineLength {
fn cache_key(&self, state: &mut CacheKeyHasher) {
state.write_u16(self.0.get());
impl LineLength {
pub const fn get(&self) -> usize {
self.0
}
}
/// Error type returned when parsing a [`LineLength`] from a string fails
pub enum ParseLineWidthError {
/// The string could not be parsed as a valid [u16]
ParseError(ParseIntError),
/// The [u16] value of the string is not a valid [LineLength]
TryFromIntError(LineLengthFromIntError),
}
impl std::fmt::Debug for ParseLineWidthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl std::fmt::Display for ParseLineWidthError {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseLineWidthError::ParseError(err) => std::fmt::Display::fmt(err, fmt),
ParseLineWidthError::TryFromIntError(err) => std::fmt::Display::fmt(err, fmt),
}
}
}
impl Error for ParseLineWidthError {}
impl FromStr for LineLength {
type Err = ParseLineWidthError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let value = u16::from_str(s).map_err(ParseLineWidthError::ParseError)?;
let value = Self::try_from(value).map_err(ParseLineWidthError::TryFromIntError)?;
Ok(value)
}
}
/// Error type returned when converting a u16 to a [`LineLength`] fails
#[derive(Clone, Copy, Debug)]
pub struct LineLengthFromIntError(pub u16);
impl TryFrom<u16> for LineLength {
type Error = LineLengthFromIntError;
fn try_from(value: u16) -> Result<Self, Self::Error> {
match NonZeroU16::try_from(value) {
Ok(value) if value.get() <= Self::MAX => Ok(LineLength(value)),
Ok(_) | Err(_) => Err(LineLengthFromIntError(value)),
}
}
}
impl std::fmt::Display for LineLengthFromIntError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"The line width must be a value between 1 and {}.",
LineLength::MAX
)
}
}
impl From<LineLength> for u16 {
fn from(value: LineLength) -> Self {
value.0.get()
}
}
impl From<LineLength> for NonZeroU16 {
fn from(value: LineLength) -> Self {
value.0
impl From<usize> for LineLength {
fn from(value: usize) -> Self {
Self(value)
}
}
@@ -114,7 +33,7 @@ impl From<LineLength> for NonZeroU16 {
/// This is used to determine if a line is too long.
/// It should be compared to a [`LineLength`].
#[derive(Clone, Copy, Debug)]
pub struct LineWidthBuilder {
pub struct LineWidth {
/// The width of the line.
width: usize,
/// The column of the line.
@@ -124,40 +43,40 @@ pub struct LineWidthBuilder {
tab_size: TabSize,
}
impl Default for LineWidthBuilder {
impl Default for LineWidth {
fn default() -> Self {
Self::new(TabSize::default())
}
}
impl PartialEq for LineWidthBuilder {
impl PartialEq for LineWidth {
fn eq(&self, other: &Self) -> bool {
self.width == other.width
}
}
impl Eq for LineWidthBuilder {}
impl Eq for LineWidth {}
impl PartialOrd for LineWidthBuilder {
impl PartialOrd for LineWidth {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for LineWidthBuilder {
impl Ord for LineWidth {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.width.cmp(&other.width)
}
}
impl LineWidthBuilder {
impl LineWidth {
pub fn get(&self) -> usize {
self.width
}
/// Creates a new `LineWidth` with the given tab size.
pub fn new(tab_size: TabSize) -> Self {
LineWidthBuilder {
LineWidth {
width: 0,
column: 0,
tab_size,
@@ -200,7 +119,7 @@ impl LineWidthBuilder {
/// Adds the given width to the line width.
/// Also adds the given width to the column.
/// It is generally better to use [`LineWidthBuilder::add_str`] or [`LineWidthBuilder::add_char`].
/// It is generally better to use [`LineWidth::add_str`] or [`LineWidth::add_char`].
/// The width and column should be the same for the corresponding text.
/// Currently, this is only used to add spaces.
#[must_use]
@@ -211,15 +130,15 @@ impl LineWidthBuilder {
}
}
impl PartialEq<LineLength> for LineWidthBuilder {
impl PartialEq<LineLength> for LineWidth {
fn eq(&self, other: &LineLength) -> bool {
self.width == (other.value() as usize)
self.width == other.0
}
}
impl PartialOrd<LineLength> for LineWidthBuilder {
impl PartialOrd<LineLength> for LineWidth {
fn partial_cmp(&self, other: &LineLength) -> Option<std::cmp::Ordering> {
self.width.partial_cmp(&(other.value() as usize))
self.width.partial_cmp(&other.0)
}
}

View File

@@ -17,7 +17,6 @@ use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_source_file::{Locator, SourceFileBuilder};
use ruff_text_size::Ranged;
use crate::autofix::{fix_file, FixResult};
use crate::checkers::ast::check_ast;
@@ -148,7 +147,7 @@ pub fn check_path(
match ruff_python_parser::parse_program_tokens(
tokens,
&path.to_string_lossy(),
source_type.is_ipynb(),
source_type.is_jupyter(),
) {
Ok(python_ast) => {
if use_ast {
@@ -268,12 +267,9 @@ pub fn check_path(
const MAX_ITERATIONS: usize = 100;
/// Add any missing `# noqa` pragmas to the source code at the given `Path`.
pub fn add_noqa_to_path(
path: &Path,
package: Option<&Path>,
source_type: PySourceType,
settings: &Settings,
) -> Result<usize> {
pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings) -> Result<usize> {
let source_type = PySourceType::from(path);
// Read the file from disk.
let contents = std::fs::read_to_string(path)?;

View File

@@ -15,7 +15,7 @@ use crate::fs;
use crate::jupyter::Notebook;
use crate::source_kind::SourceKind;
pub static WARNINGS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::default);
pub(crate) static WARNINGS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::default);
/// Warn a user once, with uniqueness determined by the given ID.
#[macro_export]

View File

@@ -3,7 +3,7 @@ use std::num::NonZeroUsize;
use colored::{Color, ColoredString, Colorize, Styles};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{TextRange, TextSize};
use similar::{ChangeTag, TextDiff};
use ruff_diagnostics::{Applicability, Fix};

View File

@@ -7,7 +7,7 @@ use colored::Colorize;
use ruff_source_file::OneIndexed;
use crate::fs::relativize_path;
use crate::jupyter::{Notebook, NotebookIndex};
use crate::jupyter::{JupyterIndex, Notebook};
use crate::message::diff::calculate_print_width;
use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
use crate::message::{
@@ -92,7 +92,7 @@ struct DisplayGroupedMessage<'a> {
show_source: bool,
row_length: NonZeroUsize,
column_length: NonZeroUsize,
jupyter_index: Option<&'a NotebookIndex>,
jupyter_index: Option<&'a JupyterIndex>,
}
impl Display for DisplayGroupedMessage<'_> {

View File

@@ -6,7 +6,6 @@ use serde_json::{json, Value};
use ruff_diagnostics::Edit;
use ruff_source_file::SourceCode;
use ruff_text_size::Ranged;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;

View File

@@ -15,7 +15,7 @@ pub use junit::JunitEmitter;
pub use pylint::PylintEmitter;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_source_file::{SourceFile, SourceLocation};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{TextRange, TextSize};
pub use text::TextEmitter;
use crate::jupyter::Notebook;
@@ -66,6 +66,14 @@ impl Message {
pub fn compute_end_location(&self) -> SourceLocation {
self.file.to_source_code().source_location(self.end())
}
pub const fn start(&self) -> TextSize {
self.range.start()
}
pub const fn end(&self) -> TextSize {
self.range.end()
}
}
impl Ord for Message {
@@ -80,12 +88,6 @@ impl PartialOrd for Message {
}
}
impl Ranged for Message {
fn range(&self) -> TextRange {
self.range
}
}
struct MessageWithLocation<'a> {
message: &'a Message,
start_location: SourceLocation,
@@ -152,7 +154,7 @@ mod tests {
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Edit, Fix};
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{TextRange, TextSize};
use crate::message::{Emitter, EmitterContext, Message};

View File

@@ -8,11 +8,11 @@ use bitflags::bitflags;
use colored::Colorize;
use ruff_source_file::{OneIndexed, SourceLocation};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{TextRange, TextSize};
use crate::fs::relativize_path;
use crate::jupyter::{Notebook, NotebookIndex};
use crate::line_width::{LineWidthBuilder, TabSize};
use crate::jupyter::{JupyterIndex, Notebook};
use crate::line_width::{LineWidth, TabSize};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
@@ -161,7 +161,7 @@ impl Display for RuleCodeAndBody<'_> {
pub(super) struct MessageCodeFrame<'a> {
pub(crate) message: &'a Message,
pub(crate) jupyter_index: Option<&'a NotebookIndex>,
pub(crate) jupyter_index: Option<&'a JupyterIndex>,
}
impl Display for MessageCodeFrame<'_> {
@@ -292,7 +292,7 @@ fn replace_whitespace(source: &str, annotation_range: TextRange) -> SourceCode {
let mut result = String::new();
let mut last_end = 0;
let mut range = annotation_range;
let mut line_width = LineWidthBuilder::new(TabSize::default());
let mut line_width = LineWidth::new(TabSize::default());
for (index, c) in source.char_indices() {
let old_width = line_width.get();

View File

@@ -8,7 +8,8 @@ use std::path::Path;
use anyhow::Result;
use itertools::Itertools;
use log::warn;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_python_ast::Ranged;
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_diagnostics::Diagnostic;
use ruff_python_trivia::indentation_at_offset;
@@ -536,7 +537,7 @@ fn add_noqa_inner(
let rule = diagnostic.kind.rule();
if !includes(rule, codes) {
matches_by_line
.entry(directive_line.start())
.entry(directive_line.range.start())
.or_insert_with(|| {
(RuleSet::default(), Some(&directive_line.directive))
})
@@ -561,7 +562,7 @@ fn add_noqa_inner(
let mut prev_end = TextSize::default();
for (offset, (rules, directive)) in matches_by_line {
output.push_str(locator.slice(TextRange::new(prev_end, offset)));
output.push_str(&locator.contents()[TextRange::new(prev_end, offset)]);
let line = locator.full_line(offset);
@@ -618,7 +619,7 @@ fn add_noqa_inner(
prev_end = offset + line.text_len();
}
output.push_str(locator.after(prev_end));
output.push_str(&locator.contents()[usize::from(prev_end)..]);
(count, output)
}
@@ -644,13 +645,6 @@ pub(crate) struct NoqaDirectiveLine<'a> {
pub(crate) matches: Vec<NoqaCode>,
}
impl Ranged for NoqaDirectiveLine<'_> {
/// The range of the `noqa` directive.
fn range(&self) -> TextRange {
self.range
}
}
#[derive(Debug, Default)]
pub(crate) struct NoqaDirectives<'a> {
inner: Vec<NoqaDirectiveLine<'a>>,

View File

@@ -2,6 +2,10 @@
use std::path::{Path, PathBuf};
use rustc_hash::FxHashMap;
use crate::resolver::{PyprojectConfig, Resolver};
// If we have a Python package layout like:
// - root/
// - foo/
@@ -47,6 +51,68 @@ pub fn detect_package_root<'a>(
current
}
/// A wrapper around `is_package` to cache filesystem lookups.
fn is_package_with_cache<'a>(
path: &'a Path,
namespace_packages: &'a [PathBuf],
package_cache: &mut FxHashMap<&'a Path, bool>,
) -> bool {
*package_cache
.entry(path)
.or_insert_with(|| is_package(path, namespace_packages))
}
/// A wrapper around `detect_package_root` to cache filesystem lookups.
fn detect_package_root_with_cache<'a>(
path: &'a Path,
namespace_packages: &'a [PathBuf],
package_cache: &mut FxHashMap<&'a Path, bool>,
) -> Option<&'a Path> {
let mut current = None;
for parent in path.ancestors() {
if !is_package_with_cache(parent, namespace_packages, package_cache) {
return current;
}
current = Some(parent);
}
current
}
/// Return a mapping from Python package to its package root.
pub fn detect_package_roots<'a>(
files: &[&'a Path],
resolver: &'a Resolver,
pyproject_config: &'a PyprojectConfig,
) -> FxHashMap<&'a Path, Option<&'a Path>> {
// Pre-populate the module cache, since the list of files could (but isn't
// required to) contain some `__init__.py` files.
let mut package_cache: FxHashMap<&Path, bool> = FxHashMap::default();
for file in files {
if file.ends_with("__init__.py") {
if let Some(parent) = file.parent() {
package_cache.insert(parent, true);
}
}
}
// Search for the package root for each file.
let mut package_roots: FxHashMap<&Path, Option<&Path>> = FxHashMap::default();
for file in files {
let namespace_packages = &resolver.resolve(file, pyproject_config).namespace_packages;
if let Some(package) = file.parent() {
if package_roots.contains_key(package) {
continue;
}
package_roots.insert(
package,
detect_package_root_with_cache(package, namespace_packages, &mut package_cache),
);
}
}
package_roots
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;

View File

@@ -196,9 +196,6 @@ pub enum Linter {
/// [Perflint](https://pypi.org/project/perflint/)
#[prefix = "PERF"]
Perflint,
/// [refurb](https://pypi.org/project/refurb/)
#[prefix = "FURB"]
Refurb,
/// Ruff-specific rules
#[prefix = "RUF"]
Ruff,

View File

@@ -4,8 +4,8 @@ use anyhow::{anyhow, Result};
use itertools::Itertools;
use ruff_diagnostics::Edit;
use ruff_python_ast::Ranged;
use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel};
use ruff_text_size::Ranged;
pub(crate) struct Renamer;

View File

@@ -5,20 +5,17 @@ use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use anyhow::Result;
use anyhow::{anyhow, bail};
use anyhow::{anyhow, bail, Result};
use ignore::{DirEntry, WalkBuilder, WalkState};
use itertools::Itertools;
use log::debug;
use path_absolutize::path_dedot;
use rustc_hash::{FxHashMap, FxHashSet};
use rustc_hash::FxHashSet;
use crate::configuration::Configuration;
use crate::pyproject;
use crate::pyproject::settings_toml;
use ruff::fs;
use ruff::packaging::is_package;
use ruff::settings::{AllSettings, Settings};
use crate::fs;
use crate::settings::configuration::Configuration;
use crate::settings::pyproject::settings_toml;
use crate::settings::{pyproject, AllSettings, Settings};
/// The configuration information from a `pyproject.toml` file.
pub struct PyprojectConfig {
@@ -48,7 +45,7 @@ impl PyprojectConfig {
/// The strategy used to discover the relevant `pyproject.toml` file for each
/// Python file.
#[derive(Debug, Copy, Clone)]
#[derive(Debug, is_macro::Is)]
pub enum PyprojectDiscoveryStrategy {
/// Use a fixed `pyproject.toml` file for all Python files (i.e., one
/// provided on the command-line).
@@ -58,16 +55,6 @@ pub enum PyprojectDiscoveryStrategy {
Hierarchical,
}
impl PyprojectDiscoveryStrategy {
pub const fn is_fixed(self) -> bool {
matches!(self, PyprojectDiscoveryStrategy::Fixed)
}
pub const fn is_hierarchical(self) -> bool {
matches!(self, PyprojectDiscoveryStrategy::Hierarchical)
}
}
/// The strategy for resolving file paths in a `pyproject.toml`.
#[derive(Copy, Clone)]
pub enum Relativity {
@@ -79,7 +66,7 @@ pub enum Relativity {
}
impl Relativity {
pub fn resolve(self, path: &Path) -> PathBuf {
pub fn resolve(&self, path: &Path) -> PathBuf {
match self {
Relativity::Parent => path
.parent()
@@ -97,7 +84,7 @@ pub struct Resolver {
impl Resolver {
/// Add a resolved [`Settings`] under a given [`PathBuf`] scope.
fn add(&mut self, path: PathBuf, settings: AllSettings) {
pub fn add(&mut self, path: PathBuf, settings: AllSettings) {
self.settings.insert(path, settings);
}
@@ -126,88 +113,31 @@ impl Resolver {
&self.resolve_all(path, pyproject_config).lib
}
/// Return a mapping from Python package to its package root.
pub fn package_roots<'a>(
&'a self,
files: &[&'a Path],
pyproject_config: &'a PyprojectConfig,
) -> FxHashMap<&'a Path, Option<&'a Path>> {
// Pre-populate the module cache, since the list of files could (but isn't
// required to) contain some `__init__.py` files.
let mut package_cache: FxHashMap<&Path, bool> = FxHashMap::default();
for file in files {
if file.ends_with("__init__.py") {
if let Some(parent) = file.parent() {
package_cache.insert(parent, true);
}
}
}
// Search for the package root for each file.
let mut package_roots: FxHashMap<&Path, Option<&Path>> = FxHashMap::default();
for file in files {
let namespace_packages = &self.resolve(file, pyproject_config).namespace_packages;
if let Some(package) = file.parent() {
if package_roots.contains_key(package) {
continue;
}
package_roots.insert(
package,
detect_package_root_with_cache(package, namespace_packages, &mut package_cache),
);
}
}
package_roots
}
/// Return an iterator over the resolved [`Settings`] in this [`Resolver`].
pub fn settings(&self) -> impl Iterator<Item = &AllSettings> {
pub fn iter(&self) -> impl Iterator<Item = &AllSettings> {
self.settings.values()
}
}
/// A wrapper around `detect_package_root` to cache filesystem lookups.
fn detect_package_root_with_cache<'a>(
path: &'a Path,
namespace_packages: &'a [PathBuf],
package_cache: &mut FxHashMap<&'a Path, bool>,
) -> Option<&'a Path> {
let mut current = None;
for parent in path.ancestors() {
if !is_package_with_cache(parent, namespace_packages, package_cache) {
return current;
}
current = Some(parent);
}
current
}
/// A wrapper around `is_package` to cache filesystem lookups.
fn is_package_with_cache<'a>(
path: &'a Path,
namespace_packages: &'a [PathBuf],
package_cache: &mut FxHashMap<&'a Path, bool>,
) -> bool {
*package_cache
.entry(path)
.or_insert_with(|| is_package(path, namespace_packages))
}
pub trait ConfigProcessor: Sync {
pub trait ConfigProcessor: Copy + Send + Sync {
fn process_config(&self, config: &mut Configuration);
}
struct NoOpProcessor;
impl ConfigProcessor for &NoOpProcessor {
fn process_config(&self, _config: &mut Configuration) {}
}
/// Recursively resolve a [`Configuration`] from a `pyproject.toml` file at the
/// specified [`Path`].
// TODO(charlie): This whole system could do with some caching. Right now, if a
// configuration file extends another in the same path, we'll re-parse the same
// file at least twice (possibly more than twice, since we'll also parse it when
// resolving the "default" configuration).
fn resolve_configuration(
pub fn resolve_configuration(
pyproject: &Path,
relativity: Relativity,
processor: &dyn ConfigProcessor,
relativity: &Relativity,
processor: impl ConfigProcessor,
) -> Result<Configuration> {
let mut seen = FxHashSet::default();
let mut stack = vec![];
@@ -250,33 +180,49 @@ fn resolve_configuration(
/// Extract the project root (scope) and [`Settings`] from a given
/// `pyproject.toml`.
fn resolve_scoped_settings(
pub fn resolve_scoped_settings(
pyproject: &Path,
relativity: Relativity,
processor: &dyn ConfigProcessor,
relativity: &Relativity,
processor: impl ConfigProcessor,
) -> Result<(PathBuf, AllSettings)> {
let configuration = resolve_configuration(pyproject, relativity, processor)?;
let project_root = relativity.resolve(pyproject);
let settings = configuration.into_all_settings(&project_root)?;
let settings = AllSettings::from_configuration(configuration, &project_root)?;
Ok((project_root, settings))
}
/// Extract the [`Settings`] from a given `pyproject.toml`.
pub fn resolve_settings(pyproject: &Path, relativity: &Relativity) -> Result<AllSettings> {
let (_project_root, settings) = resolve_scoped_settings(pyproject, relativity, &NoOpProcessor)?;
Ok(settings)
}
/// Extract the [`Settings`] from a given `pyproject.toml` and process the
/// configuration with the given [`ConfigProcessor`].
pub fn resolve_settings_with_processor(
pyproject: &Path,
relativity: Relativity,
processor: &dyn ConfigProcessor,
relativity: &Relativity,
processor: impl ConfigProcessor,
) -> Result<AllSettings> {
let (_project_root, settings) = resolve_scoped_settings(pyproject, relativity, processor)?;
Ok(settings)
}
/// Return `true` if the given file should be ignored based on the exclusion
/// criteria.
fn match_exclusion<P: AsRef<Path>, R: AsRef<Path>>(
file_path: P,
file_basename: R,
exclusion: &globset::GlobSet,
) -> bool {
exclusion.is_match(file_path) || exclusion.is_match(file_basename)
}
/// Find all Python (`.py`, `.pyi` and `.ipynb` files) in a set of paths.
pub fn python_files_in_path(
paths: &[PathBuf],
pyproject_config: &PyprojectConfig,
processor: &dyn ConfigProcessor,
processor: impl ConfigProcessor,
) -> Result<(Vec<Result<DirEntry, ignore::Error>>, Resolver)> {
// Normalize every path (e.g., convert from relative to absolute).
let mut paths: Vec<PathBuf> = paths.iter().map(fs::normalize_path).unique().collect();
@@ -290,7 +236,7 @@ pub fn python_files_in_path(
if seen.insert(ancestor) {
if let Some(pyproject) = settings_toml(ancestor)? {
let (root, settings) =
resolve_scoped_settings(&pyproject, Relativity::Parent, processor)?;
resolve_scoped_settings(&pyproject, &Relativity::Parent, processor)?;
resolver.add(root, settings);
}
}
@@ -362,7 +308,7 @@ pub fn python_files_in_path(
match settings_toml(entry.path()) {
Ok(Some(pyproject)) => match resolve_scoped_settings(
&pyproject,
Relativity::Parent,
&Relativity::Parent,
processor,
) {
Ok((root, settings)) => {
@@ -422,7 +368,7 @@ pub fn python_files_in_path(
pub fn python_file_at_path(
path: &Path,
pyproject_config: &PyprojectConfig,
processor: &dyn ConfigProcessor,
processor: impl ConfigProcessor,
) -> Result<bool> {
if !pyproject_config.settings.lib.force_exclude {
return Ok(true);
@@ -437,7 +383,7 @@ pub fn python_file_at_path(
for ancestor in path.ancestors() {
if let Some(pyproject) = settings_toml(ancestor)? {
let (root, settings) =
resolve_scoped_settings(&pyproject, Relativity::Parent, processor)?;
resolve_scoped_settings(&pyproject, &Relativity::Parent, processor)?;
resolver.add(root, settings);
}
}
@@ -482,16 +428,6 @@ fn is_file_excluded(
false
}
/// Return `true` if the given file should be ignored based on the exclusion
/// criteria.
fn match_exclusion<P: AsRef<Path>, R: AsRef<Path>>(
file_path: P,
file_basename: R,
exclusion: &globset::GlobSet,
) -> bool {
exclusion.is_match(file_path) || exclusion.is_match(file_basename)
}
#[cfg(test)]
mod tests {
use std::fs::{create_dir, File};
@@ -503,92 +439,14 @@ mod tests {
use path_absolutize::Absolutize;
use tempfile::TempDir;
use crate::configuration::Configuration;
use crate::pyproject::find_settings_toml;
use ruff::settings::types::FilePattern;
use ruff::settings::AllSettings;
use crate::resolver::{
is_file_excluded, match_exclusion, python_files_in_path, resolve_settings_with_processor,
ConfigProcessor, PyprojectConfig, PyprojectDiscoveryStrategy, Relativity, Resolver,
NoOpProcessor, PyprojectConfig, PyprojectDiscoveryStrategy, Relativity, Resolver,
};
use crate::tests::test_resource_path;
struct NoOpProcessor;
impl ConfigProcessor for NoOpProcessor {
fn process_config(&self, _config: &mut Configuration) {}
}
#[test]
fn rooted_exclusion() -> Result<()> {
let package_root = test_resource_path("package");
let resolver = Resolver::default();
let pyproject_config = PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical,
resolve_settings_with_processor(
&find_settings_toml(&package_root)?.unwrap(),
Relativity::Parent,
&NoOpProcessor,
)?,
None,
);
// src/app.py should not be excluded even if it lives in a hierarchy that should
// be excluded by virtue of the pyproject.toml having `resources/*` in
// it.
assert!(!is_file_excluded(
&package_root.join("src/app.py"),
&resolver,
&pyproject_config,
));
// However, resources/ignored.py should be ignored, since that `resources` is
// beneath the package root.
assert!(is_file_excluded(
&package_root.join("resources/ignored.py"),
&resolver,
&pyproject_config,
));
Ok(())
}
#[test]
fn find_python_files() -> Result<()> {
// Initialize the filesystem:
// root
// ├── file1.py
// ├── dir1.py
// │ └── file2.py
// └── dir2.py
let tmp_dir = TempDir::new()?;
let root = tmp_dir.path();
let file1 = root.join("file1.py");
let dir1 = root.join("dir1.py");
let file2 = dir1.join("file2.py");
let dir2 = root.join("dir2.py");
File::create(&file1)?;
create_dir(dir1)?;
File::create(&file2)?;
create_dir(dir2)?;
let (paths, _) = python_files_in_path(
&[root.to_path_buf()],
&PyprojectConfig::new(
PyprojectDiscoveryStrategy::Fixed,
AllSettings::default(),
None,
),
&NoOpProcessor,
)?;
let paths = paths
.iter()
.flatten()
.map(ignore::DirEntry::path)
.sorted()
.collect::<Vec<_>>();
assert_eq!(paths, &[file2, file1]);
Ok(())
}
use crate::settings::pyproject::find_settings_toml;
use crate::settings::types::FilePattern;
use crate::settings::AllSettings;
use crate::test::test_resource_path;
fn make_exclusion(file_pattern: FilePattern) -> GlobSet {
let mut builder = globset::GlobSetBuilder::new();
@@ -720,4 +578,74 @@ mod tests {
&make_exclusion(exclude),
));
}
#[test]
fn rooted_exclusion() -> Result<()> {
let package_root = test_resource_path("package");
let resolver = Resolver::default();
let pyproject_config = PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical,
resolve_settings_with_processor(
&find_settings_toml(&package_root)?.unwrap(),
&Relativity::Parent,
&NoOpProcessor,
)?,
None,
);
// src/app.py should not be excluded even if it lives in a hierarchy that should
// be excluded by virtue of the pyproject.toml having `resources/*` in
// it.
assert!(!is_file_excluded(
&package_root.join("src/app.py"),
&resolver,
&pyproject_config,
));
// However, resources/ignored.py should be ignored, since that `resources` is
// beneath the package root.
assert!(is_file_excluded(
&package_root.join("resources/ignored.py"),
&resolver,
&pyproject_config,
));
Ok(())
}
#[test]
fn find_python_files() -> Result<()> {
// Initialize the filesystem:
// root
// ├── file1.py
// ├── dir1.py
// │ └── file2.py
// └── dir2.py
let tmp_dir = TempDir::new()?;
let root = tmp_dir.path();
let file1 = root.join("file1.py");
let dir1 = root.join("dir1.py");
let file2 = dir1.join("file2.py");
let dir2 = root.join("dir2.py");
File::create(&file1)?;
create_dir(dir1)?;
File::create(&file2)?;
create_dir(dir2)?;
let (paths, _) = python_files_in_path(
&[root.to_path_buf()],
&PyprojectConfig::new(
PyprojectDiscoveryStrategy::Fixed,
AllSettings::default(),
None,
),
&NoOpProcessor,
)?;
let paths = paths
.iter()
.flatten()
.map(ignore::DirEntry::path)
.sorted()
.collect::<Vec<_>>();
assert_eq!(paths, &[file2, file1]);
Ok(())
}
}

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