Compare commits
69 Commits
v0.2.1
...
implicit-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc24d01b2e | ||
|
|
5a9d656bc4 | ||
|
|
33184dc6a4 | ||
|
|
bb8d2034e2 | ||
|
|
f40e012b4e | ||
|
|
3e9d761b13 | ||
|
|
46db3f96ac | ||
|
|
6f9c128d77 | ||
|
|
6380c90031 | ||
|
|
d96a0dbe57 | ||
|
|
180920fdd9 | ||
|
|
1ccd8354c1 | ||
|
|
dd0ba16a79 | ||
|
|
609d0a9a65 | ||
|
|
8fba97f72f | ||
|
|
5bc0d9c324 | ||
|
|
cf77eeb913 | ||
|
|
3f4dd01e7a | ||
|
|
edfe8421ec | ||
|
|
ab2253db03 | ||
|
|
33ac2867b7 | ||
|
|
0304623878 | ||
|
|
e2785f3fb6 | ||
|
|
90f8e4baf4 | ||
|
|
8657a392ff | ||
|
|
4946a1876f | ||
|
|
6dc1b21917 | ||
|
|
2e1160e74c | ||
|
|
37ff436e4e | ||
|
|
341c2698a7 | ||
|
|
a50e2787df | ||
|
|
25868d0371 | ||
|
|
af2cba7c0a | ||
|
|
8ec56277e9 | ||
|
|
b21ba71ef4 | ||
|
|
d387d0ba82 | ||
|
|
6f0e4ad332 | ||
|
|
7ca515c0aa | ||
|
|
1ce07d65bd | ||
|
|
00ef01d035 | ||
|
|
52ebfc9718 | ||
|
|
12a91f4e90 | ||
|
|
b4f2882b72 | ||
|
|
49fe1b85f2 | ||
|
|
bd8123c0d8 | ||
|
|
49c5e715f9 | ||
|
|
fe7d965334 | ||
|
|
9027169125 | ||
|
|
688177ff6a | ||
|
|
eb2784c495 | ||
|
|
6fffde72e7 | ||
|
|
ad313b9089 | ||
|
|
f76a3e8502 | ||
|
|
ed07fa08bd | ||
|
|
45937426c7 | ||
|
|
533dcfb114 | ||
|
|
bc023f47a1 | ||
|
|
aa38307415 | ||
|
|
e9ddd4819a | ||
|
|
fdb5eefb33 | ||
|
|
daae28efc7 | ||
|
|
75553ab1c0 | ||
|
|
c34908f5ad | ||
|
|
a662c2447c | ||
|
|
df7fb95cbc | ||
|
|
83195a6030 | ||
|
|
d31d09d7cd | ||
|
|
0f436b71f3 | ||
|
|
cd5bcd815d |
8
.config/nextest.toml
Normal file
8
.config/nextest.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[profile.ci]
|
||||
# Print out output for failing tests as soon as they fail, and also at the end
|
||||
# of the run (for easy scrollability).
|
||||
failure-output = "immediate-final"
|
||||
# Do not cancel the test run on the first failure.
|
||||
fail-fast = false
|
||||
|
||||
status-level = "skip"
|
||||
26
.github/workflows/ci.yaml
vendored
26
.github/workflows/ci.yaml
vendored
@@ -111,16 +111,23 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
run: cargo insta test --all --exclude ruff_dev --all-features --unreferenced reject
|
||||
- name: "Run dev tests"
|
||||
# e.g. generating the schema — these should not run with all features enabled
|
||||
run: cargo insta test -p ruff_dev --unreferenced reject
|
||||
shell: bash
|
||||
env:
|
||||
NEXTEST_PROFILE: "ci"
|
||||
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
|
||||
|
||||
# Check for broken links in the documentation.
|
||||
- run: cargo doc --all --no-deps
|
||||
env:
|
||||
@@ -141,15 +148,16 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo insta"
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
tool: cargo-nextest
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
# We can't reject unreferenced snapshots on windows because flake8_executable can't run on windows
|
||||
run: cargo insta test --all --exclude ruff_dev --all-features
|
||||
run: |
|
||||
cargo nextest run --all-features --profile ci
|
||||
cargo test --all-features --doc
|
||||
|
||||
cargo-test-wasm:
|
||||
name: "cargo test (wasm)"
|
||||
@@ -410,7 +418,7 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@v0.8.0
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
|
||||
- name: "Install Rust toolchain"
|
||||
|
||||
2
.github/workflows/docs.yaml
vendored
2
.github/workflows/docs.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@v0.8.0
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
|
||||
- name: "Install Rust toolchain"
|
||||
|
||||
@@ -7,7 +7,7 @@ within a source file).
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`refurn`\] Implement `missing-f-string-syntax` (`RUF027`) ([#9728](https://github.com/astral-sh/ruff/pull/9728))
|
||||
- \[`refurb`\] Implement `missing-f-string-syntax` (`RUF027`) ([#9728](https://github.com/astral-sh/ruff/pull/9728))
|
||||
- Format module-level docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725))
|
||||
|
||||
### Formatter
|
||||
|
||||
@@ -26,6 +26,10 @@ Welcome! We're happy to have you here. Thank you in advance for your contributio
|
||||
- [`cargo dev`](#cargo-dev)
|
||||
- [Subsystems](#subsystems)
|
||||
- [Compilation Pipeline](#compilation-pipeline)
|
||||
- [Import Categorization](#import-categorization)
|
||||
- [Project root](#project-root)
|
||||
- [Package root](#package-root)
|
||||
- [Import categorization](#import-categorization-1)
|
||||
|
||||
## The Basics
|
||||
|
||||
@@ -63,7 +67,7 @@ You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests:
|
||||
cargo install cargo-insta
|
||||
```
|
||||
|
||||
and pre-commit to run some validation checks:
|
||||
And you'll need pre-commit to run some validation checks:
|
||||
|
||||
```shell
|
||||
pipx install pre-commit # or `pip install pre-commit` if you have a virtualenv
|
||||
@@ -76,6 +80,16 @@ when making a commit:
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
We recommend [nextest](https://nexte.st/) to run Ruff's test suite (via `cargo nextest run`),
|
||||
though it's not strictly necessary:
|
||||
|
||||
```shell
|
||||
cargo install cargo-nextest --locked
|
||||
```
|
||||
|
||||
Throughout this guide, any usages of `cargo test` can be replaced with `cargo nextest run`,
|
||||
if you choose to install `nextest`.
|
||||
|
||||
### Development
|
||||
|
||||
After cloning the repository, run Ruff locally from the repository root with:
|
||||
@@ -231,7 +245,7 @@ Once you've completed the code for the rule itself, you can define tests with th
|
||||
For example, if you're adding a new rule named `E402`, you would run:
|
||||
|
||||
```shell
|
||||
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402
|
||||
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --preview --select E402
|
||||
```
|
||||
|
||||
**Note:** Only a subset of rules are enabled by default. When testing a new rule, ensure that
|
||||
@@ -373,6 +387,11 @@ We have several ways of benchmarking and profiling Ruff:
|
||||
- Microbenchmarks which run the linter or the formatter on individual files. These run on pull requests.
|
||||
- Profiling the linter on either the microbenchmarks or entire projects
|
||||
|
||||
> \[!NOTE\]
|
||||
> When running benchmarks, ensure that your CPU is otherwise idle (e.g., close any background
|
||||
> applications, like web browsers). You may also want to switch your CPU to a "performance"
|
||||
> mode, if it exists, especially when benchmarking short-lived processes.
|
||||
|
||||
### CPython Benchmark
|
||||
|
||||
First, clone [CPython](https://github.com/python/cpython). It's a large and diverse Python codebase,
|
||||
|
||||
696
Cargo.lock
generated
696
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -19,8 +19,9 @@ argfile = { version = "0.1.6" }
|
||||
assert_cmd = { version = "2.0.13" }
|
||||
bincode = { version = "1.3.3" }
|
||||
bitflags = { version = "2.4.1" }
|
||||
bstr = { version = "1.9.0" }
|
||||
cachedir = { version = "0.3.1" }
|
||||
chrono = { version = "0.4.33", default-features = false, features = ["clock"] }
|
||||
chrono = { version = "0.4.34", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.4.18", features = ["derive"] }
|
||||
clap_complete_command = { version = "0.5.1" }
|
||||
clearscreen = { version = "2.0.0" }
|
||||
@@ -43,7 +44,7 @@ hexf-parse = { version ="0.2.1"}
|
||||
ignore = { version = "0.4.22" }
|
||||
imara-diff ={ version = "0.1.5"}
|
||||
imperative = { version = "1.0.4" }
|
||||
indicatif ={ version = "0.17.7"}
|
||||
indicatif ={ version = "0.17.8"}
|
||||
indoc ={ version = "2.0.4"}
|
||||
insta = { version = "1.34.0", feature = ["filters", "glob"] }
|
||||
insta-cmd = { version = "0.4.0" }
|
||||
@@ -65,7 +66,7 @@ pathdiff = { version = "0.2.1" }
|
||||
pep440_rs = { version = "0.4.0", features = ["serde"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
proc-macro2 = { version = "1.0.78" }
|
||||
pyproject-toml = { version = "0.8.2" }
|
||||
pyproject-toml = { version = "0.9.0" }
|
||||
quick-junit = { version = "0.3.5" }
|
||||
quote = { version = "1.0.23" }
|
||||
rand = { version = "0.8.5" }
|
||||
@@ -91,7 +92,7 @@ strum_macros = { version = "0.25.3" }
|
||||
syn = { version = "2.0.40" }
|
||||
tempfile = { version ="3.9.0"}
|
||||
test-case = { version = "3.3.1" }
|
||||
thiserror = { version = "1.0.51" }
|
||||
thiserror = { version = "1.0.57" }
|
||||
tikv-jemallocator = { version ="0.5.0"}
|
||||
toml = { version = "0.8.9" }
|
||||
tracing = { version = "0.1.40" }
|
||||
|
||||
@@ -48,7 +48,9 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
shellexpand = { workspace = true }
|
||||
strum = { workspace = true, features = [] }
|
||||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
walkdir = { workspace = true }
|
||||
wild = { workspace = true }
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Formatter;
|
||||
use std::path::PathBuf;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::bail;
|
||||
use clap::builder::{TypedValueParser, ValueParserFactory};
|
||||
use clap::{command, Parser};
|
||||
use colored::Colorize;
|
||||
use path_absolutize::path_dedot;
|
||||
use regex::Regex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use toml;
|
||||
|
||||
use ruff_linter::line_width::LineLength;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
@@ -19,7 +25,7 @@ use ruff_linter::{warn_user, RuleParser, RuleSelector, RuleSelectorParser};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
||||
use ruff_workspace::options::PycodestyleOptions;
|
||||
use ruff_workspace::options::{Options, PycodestyleOptions};
|
||||
use ruff_workspace::resolver::ConfigurationTransformer;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -155,10 +161,20 @@ pub struct CheckCommand {
|
||||
preview: bool,
|
||||
#[clap(long, overrides_with("preview"), hide = true)]
|
||||
no_preview: bool,
|
||||
/// Path to the `pyproject.toml` or `ruff.toml` file to use for
|
||||
/// configuration.
|
||||
#[arg(long, conflicts_with = "isolated")]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Either a path to a TOML configuration file (`pyproject.toml` or `ruff.toml`),
|
||||
/// or a TOML `<KEY> = <VALUE>` pair
|
||||
/// (such as you might find in a `ruff.toml` configuration file)
|
||||
/// overriding a specific configuration option.
|
||||
/// Overrides of individual settings using this option always take precedence
|
||||
/// over all configuration files, including configuration files that were also
|
||||
/// specified using `--config`.
|
||||
#[arg(
|
||||
long,
|
||||
action = clap::ArgAction::Append,
|
||||
value_name = "CONFIG_OPTION",
|
||||
value_parser = ConfigArgumentParser,
|
||||
)]
|
||||
pub config: Vec<SingleConfigArgument>,
|
||||
/// Comma-separated list of rule codes to enable (or ALL, to enable all rules).
|
||||
#[arg(
|
||||
long,
|
||||
@@ -291,7 +307,15 @@ pub struct CheckCommand {
|
||||
#[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")]
|
||||
pub no_cache: bool,
|
||||
/// Ignore all configuration files.
|
||||
#[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")]
|
||||
//
|
||||
// Note: We can't mark this as conflicting with `--config` here
|
||||
// as `--config` can be used for specifying configuration overrides
|
||||
// as well as configuration files.
|
||||
// Specifying a configuration file conflicts with `--isolated`;
|
||||
// specifying a configuration override does not.
|
||||
// If a user specifies `ruff check --isolated --config=ruff.toml`,
|
||||
// we emit an error later on, after the initial parsing by clap.
|
||||
#[arg(long, help_heading = "Miscellaneous")]
|
||||
pub isolated: bool,
|
||||
/// Path to the cache directory.
|
||||
#[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")]
|
||||
@@ -384,9 +408,20 @@ pub struct FormatCommand {
|
||||
/// difference between the current file and how the formatted file would look like.
|
||||
#[arg(long)]
|
||||
pub diff: bool,
|
||||
/// Path to the `pyproject.toml` or `ruff.toml` file to use for configuration.
|
||||
#[arg(long, conflicts_with = "isolated")]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Either a path to a TOML configuration file (`pyproject.toml` or `ruff.toml`),
|
||||
/// or a TOML `<KEY> = <VALUE>` pair
|
||||
/// (such as you might find in a `ruff.toml` configuration file)
|
||||
/// overriding a specific configuration option.
|
||||
/// Overrides of individual settings using this option always take precedence
|
||||
/// over all configuration files, including configuration files that were also
|
||||
/// specified using `--config`.
|
||||
#[arg(
|
||||
long,
|
||||
action = clap::ArgAction::Append,
|
||||
value_name = "CONFIG_OPTION",
|
||||
value_parser = ConfigArgumentParser,
|
||||
)]
|
||||
pub config: Vec<SingleConfigArgument>,
|
||||
|
||||
/// Disable cache reads.
|
||||
#[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")]
|
||||
@@ -428,7 +463,15 @@ pub struct FormatCommand {
|
||||
#[arg(long, help_heading = "Format configuration")]
|
||||
pub line_length: Option<LineLength>,
|
||||
/// Ignore all configuration files.
|
||||
#[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")]
|
||||
//
|
||||
// Note: We can't mark this as conflicting with `--config` here
|
||||
// as `--config` can be used for specifying configuration overrides
|
||||
// as well as configuration files.
|
||||
// Specifying a configuration file conflicts with `--isolated`;
|
||||
// specifying a configuration override does not.
|
||||
// If a user specifies `ruff check --isolated --config=ruff.toml`,
|
||||
// we emit an error later on, after the initial parsing by clap.
|
||||
#[arg(long, help_heading = "Miscellaneous")]
|
||||
pub isolated: bool,
|
||||
/// The name of the file when passing it through stdin.
|
||||
#[arg(long, help_heading = "Miscellaneous")]
|
||||
@@ -515,101 +558,181 @@ impl From<&LogLevelArgs> for LogLevel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration-related arguments passed via the CLI.
|
||||
#[derive(Default)]
|
||||
pub struct ConfigArguments {
|
||||
/// Path to a pyproject.toml or ruff.toml configuration file (etc.).
|
||||
/// Either 0 or 1 configuration file paths may be provided on the command line.
|
||||
config_file: Option<PathBuf>,
|
||||
/// Overrides provided via the `--config "KEY=VALUE"` option.
|
||||
/// An arbitrary number of these overrides may be provided on the command line.
|
||||
/// These overrides take precedence over all configuration files,
|
||||
/// even configuration files that were also specified using `--config`.
|
||||
overrides: Configuration,
|
||||
/// Overrides provided via dedicated flags such as `--line-length` etc.
|
||||
/// These overrides take precedence over all configuration files,
|
||||
/// and also over all overrides specified using any `--config "KEY=VALUE"` flags.
|
||||
per_flag_overrides: ExplicitConfigOverrides,
|
||||
}
|
||||
|
||||
impl ConfigArguments {
|
||||
pub fn config_file(&self) -> Option<&Path> {
|
||||
self.config_file.as_deref()
|
||||
}
|
||||
|
||||
fn from_cli_arguments(
|
||||
config_options: Vec<SingleConfigArgument>,
|
||||
per_flag_overrides: ExplicitConfigOverrides,
|
||||
isolated: bool,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut new = Self {
|
||||
per_flag_overrides,
|
||||
..Self::default()
|
||||
};
|
||||
|
||||
for option in config_options {
|
||||
match option {
|
||||
SingleConfigArgument::SettingsOverride(overridden_option) => {
|
||||
let overridden_option = Arc::try_unwrap(overridden_option)
|
||||
.unwrap_or_else(|option| option.deref().clone());
|
||||
new.overrides = new.overrides.combine(Configuration::from_options(
|
||||
overridden_option,
|
||||
None,
|
||||
&path_dedot::CWD,
|
||||
)?);
|
||||
}
|
||||
SingleConfigArgument::FilePath(path) => {
|
||||
if isolated {
|
||||
bail!(
|
||||
"\
|
||||
The argument `--config={}` cannot be used with `--isolated`
|
||||
|
||||
tip: You cannot specify a configuration file and also specify `--isolated`,
|
||||
as `--isolated` causes ruff to ignore all configuration files.
|
||||
For more information, try `--help`.
|
||||
",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
if let Some(ref config_file) = new.config_file {
|
||||
let (first, second) = (config_file.display(), path.display());
|
||||
bail!(
|
||||
"\
|
||||
You cannot specify more than one configuration file on the command line.
|
||||
|
||||
tip: remove either `--config={first}` or `--config={second}`.
|
||||
For more information, try `--help`.
|
||||
"
|
||||
);
|
||||
}
|
||||
new.config_file = Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(new)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigurationTransformer for ConfigArguments {
|
||||
fn transform(&self, config: Configuration) -> Configuration {
|
||||
let with_config_overrides = self.overrides.clone().combine(config);
|
||||
self.per_flag_overrides.transform(with_config_overrides)
|
||||
}
|
||||
}
|
||||
|
||||
impl CheckCommand {
|
||||
/// Partition the CLI into command-line arguments and configuration
|
||||
/// overrides.
|
||||
pub fn partition(self) -> (CheckArguments, CliOverrides) {
|
||||
(
|
||||
CheckArguments {
|
||||
add_noqa: self.add_noqa,
|
||||
config: self.config,
|
||||
diff: self.diff,
|
||||
ecosystem_ci: self.ecosystem_ci,
|
||||
exit_non_zero_on_fix: self.exit_non_zero_on_fix,
|
||||
exit_zero: self.exit_zero,
|
||||
files: self.files,
|
||||
ignore_noqa: self.ignore_noqa,
|
||||
isolated: self.isolated,
|
||||
no_cache: self.no_cache,
|
||||
output_file: self.output_file,
|
||||
show_files: self.show_files,
|
||||
show_settings: self.show_settings,
|
||||
statistics: self.statistics,
|
||||
stdin_filename: self.stdin_filename,
|
||||
watch: self.watch,
|
||||
},
|
||||
CliOverrides {
|
||||
dummy_variable_rgx: self.dummy_variable_rgx,
|
||||
exclude: self.exclude,
|
||||
extend_exclude: self.extend_exclude,
|
||||
extend_fixable: self.extend_fixable,
|
||||
extend_ignore: self.extend_ignore,
|
||||
extend_per_file_ignores: self.extend_per_file_ignores,
|
||||
extend_select: self.extend_select,
|
||||
extend_unfixable: self.extend_unfixable,
|
||||
fixable: self.fixable,
|
||||
ignore: self.ignore,
|
||||
line_length: self.line_length,
|
||||
per_file_ignores: self.per_file_ignores,
|
||||
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
|
||||
respect_gitignore: resolve_bool_arg(
|
||||
self.respect_gitignore,
|
||||
self.no_respect_gitignore,
|
||||
),
|
||||
select: self.select,
|
||||
target_version: self.target_version,
|
||||
unfixable: self.unfixable,
|
||||
// TODO(charlie): Included in `pyproject.toml`, but not inherited.
|
||||
cache_dir: self.cache_dir,
|
||||
fix: resolve_bool_arg(self.fix, self.no_fix),
|
||||
fix_only: resolve_bool_arg(self.fix_only, self.no_fix_only),
|
||||
unsafe_fixes: resolve_bool_arg(self.unsafe_fixes, self.no_unsafe_fixes)
|
||||
.map(UnsafeFixes::from),
|
||||
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
|
||||
output_format: resolve_output_format(
|
||||
self.output_format,
|
||||
resolve_bool_arg(self.show_source, self.no_show_source),
|
||||
resolve_bool_arg(self.preview, self.no_preview).unwrap_or_default(),
|
||||
),
|
||||
show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes),
|
||||
extension: self.extension,
|
||||
},
|
||||
)
|
||||
pub fn partition(self) -> anyhow::Result<(CheckArguments, ConfigArguments)> {
|
||||
let check_arguments = CheckArguments {
|
||||
add_noqa: self.add_noqa,
|
||||
diff: self.diff,
|
||||
ecosystem_ci: self.ecosystem_ci,
|
||||
exit_non_zero_on_fix: self.exit_non_zero_on_fix,
|
||||
exit_zero: self.exit_zero,
|
||||
files: self.files,
|
||||
ignore_noqa: self.ignore_noqa,
|
||||
isolated: self.isolated,
|
||||
no_cache: self.no_cache,
|
||||
output_file: self.output_file,
|
||||
show_files: self.show_files,
|
||||
show_settings: self.show_settings,
|
||||
statistics: self.statistics,
|
||||
stdin_filename: self.stdin_filename,
|
||||
watch: self.watch,
|
||||
};
|
||||
|
||||
let cli_overrides = ExplicitConfigOverrides {
|
||||
dummy_variable_rgx: self.dummy_variable_rgx,
|
||||
exclude: self.exclude,
|
||||
extend_exclude: self.extend_exclude,
|
||||
extend_fixable: self.extend_fixable,
|
||||
extend_ignore: self.extend_ignore,
|
||||
extend_per_file_ignores: self.extend_per_file_ignores,
|
||||
extend_select: self.extend_select,
|
||||
extend_unfixable: self.extend_unfixable,
|
||||
fixable: self.fixable,
|
||||
ignore: self.ignore,
|
||||
line_length: self.line_length,
|
||||
per_file_ignores: self.per_file_ignores,
|
||||
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
|
||||
respect_gitignore: resolve_bool_arg(self.respect_gitignore, self.no_respect_gitignore),
|
||||
select: self.select,
|
||||
target_version: self.target_version,
|
||||
unfixable: self.unfixable,
|
||||
// TODO(charlie): Included in `pyproject.toml`, but not inherited.
|
||||
cache_dir: self.cache_dir,
|
||||
fix: resolve_bool_arg(self.fix, self.no_fix),
|
||||
fix_only: resolve_bool_arg(self.fix_only, self.no_fix_only),
|
||||
unsafe_fixes: resolve_bool_arg(self.unsafe_fixes, self.no_unsafe_fixes)
|
||||
.map(UnsafeFixes::from),
|
||||
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
|
||||
output_format: resolve_output_format(
|
||||
self.output_format,
|
||||
resolve_bool_arg(self.show_source, self.no_show_source),
|
||||
resolve_bool_arg(self.preview, self.no_preview).unwrap_or_default(),
|
||||
),
|
||||
show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes),
|
||||
extension: self.extension,
|
||||
};
|
||||
|
||||
let config_args =
|
||||
ConfigArguments::from_cli_arguments(self.config, cli_overrides, self.isolated)?;
|
||||
Ok((check_arguments, config_args))
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatCommand {
|
||||
/// Partition the CLI into command-line arguments and configuration
|
||||
/// overrides.
|
||||
pub fn partition(self) -> (FormatArguments, CliOverrides) {
|
||||
(
|
||||
FormatArguments {
|
||||
check: self.check,
|
||||
diff: self.diff,
|
||||
config: self.config,
|
||||
files: self.files,
|
||||
isolated: self.isolated,
|
||||
no_cache: self.no_cache,
|
||||
stdin_filename: self.stdin_filename,
|
||||
range: self.range,
|
||||
},
|
||||
CliOverrides {
|
||||
line_length: self.line_length,
|
||||
respect_gitignore: resolve_bool_arg(
|
||||
self.respect_gitignore,
|
||||
self.no_respect_gitignore,
|
||||
),
|
||||
exclude: self.exclude,
|
||||
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
|
||||
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
|
||||
target_version: self.target_version,
|
||||
cache_dir: self.cache_dir,
|
||||
extension: self.extension,
|
||||
pub fn partition(self) -> anyhow::Result<(FormatArguments, ConfigArguments)> {
|
||||
let format_arguments = FormatArguments {
|
||||
check: self.check,
|
||||
diff: self.diff,
|
||||
files: self.files,
|
||||
isolated: self.isolated,
|
||||
no_cache: self.no_cache,
|
||||
stdin_filename: self.stdin_filename,
|
||||
range: self.range,
|
||||
};
|
||||
|
||||
// Unsupported on the formatter CLI, but required on `Overrides`.
|
||||
..CliOverrides::default()
|
||||
},
|
||||
)
|
||||
let cli_overrides = ExplicitConfigOverrides {
|
||||
line_length: self.line_length,
|
||||
respect_gitignore: resolve_bool_arg(self.respect_gitignore, self.no_respect_gitignore),
|
||||
exclude: self.exclude,
|
||||
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
|
||||
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
|
||||
target_version: self.target_version,
|
||||
cache_dir: self.cache_dir,
|
||||
extension: self.extension,
|
||||
|
||||
// Unsupported on the formatter CLI, but required on `Overrides`.
|
||||
..ExplicitConfigOverrides::default()
|
||||
};
|
||||
|
||||
let config_args =
|
||||
ConfigArguments::from_cli_arguments(self.config, cli_overrides, self.isolated)?;
|
||||
Ok((format_arguments, config_args))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,6 +745,154 @@ fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TomlParseFailureKind {
|
||||
SyntaxError,
|
||||
UnknownOption,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TomlParseFailureKind {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let display = match self {
|
||||
Self::SyntaxError => "The supplied argument is not valid TOML",
|
||||
Self::UnknownOption => {
|
||||
"Could not parse the supplied argument as a `ruff.toml` configuration option"
|
||||
}
|
||||
};
|
||||
write!(f, "{display}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TomlParseFailure {
|
||||
kind: TomlParseFailureKind,
|
||||
underlying_error: toml::de::Error,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TomlParseFailure {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let TomlParseFailure {
|
||||
kind,
|
||||
underlying_error,
|
||||
} = self;
|
||||
let display = format!("{kind}:\n\n{underlying_error}");
|
||||
write!(f, "{}", display.trim_end())
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumeration to represent a single `--config` argument
|
||||
/// passed via the CLI.
|
||||
///
|
||||
/// Using the `--config` flag, users may pass 0 or 1 paths
|
||||
/// to configuration files and an arbitrary number of
|
||||
/// "inline TOML" overrides for specific settings.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```sh
|
||||
/// ruff check --config "path/to/ruff.toml" --config "extend-select=['E501', 'F841']" --config "lint.per-file-ignores = {'some_file.py' = ['F841']}"
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SingleConfigArgument {
|
||||
FilePath(PathBuf),
|
||||
SettingsOverride(Arc<Options>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigArgumentParser;
|
||||
|
||||
impl ValueParserFactory for SingleConfigArgument {
|
||||
type Parser = ConfigArgumentParser;
|
||||
|
||||
fn value_parser() -> Self::Parser {
|
||||
ConfigArgumentParser
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedValueParser for ConfigArgumentParser {
|
||||
type Value = SingleConfigArgument;
|
||||
|
||||
fn parse_ref(
|
||||
&self,
|
||||
cmd: &clap::Command,
|
||||
arg: Option<&clap::Arg>,
|
||||
value: &std::ffi::OsStr,
|
||||
) -> Result<Self::Value, clap::Error> {
|
||||
let path_to_config_file = PathBuf::from(value);
|
||||
if path_to_config_file.exists() {
|
||||
return Ok(SingleConfigArgument::FilePath(path_to_config_file));
|
||||
}
|
||||
|
||||
let value = value
|
||||
.to_str()
|
||||
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
|
||||
|
||||
let toml_parse_error = match toml::Table::from_str(value) {
|
||||
Ok(table) => match table.try_into() {
|
||||
Ok(option) => return Ok(SingleConfigArgument::SettingsOverride(Arc::new(option))),
|
||||
Err(underlying_error) => TomlParseFailure {
|
||||
kind: TomlParseFailureKind::UnknownOption,
|
||||
underlying_error,
|
||||
},
|
||||
},
|
||||
Err(underlying_error) => TomlParseFailure {
|
||||
kind: TomlParseFailureKind::SyntaxError,
|
||||
underlying_error,
|
||||
},
|
||||
};
|
||||
|
||||
let mut new_error = clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
|
||||
if let Some(arg) = arg {
|
||||
new_error.insert(
|
||||
clap::error::ContextKind::InvalidArg,
|
||||
clap::error::ContextValue::String(arg.to_string()),
|
||||
);
|
||||
}
|
||||
new_error.insert(
|
||||
clap::error::ContextKind::InvalidValue,
|
||||
clap::error::ContextValue::String(value.to_string()),
|
||||
);
|
||||
|
||||
// small hack so that multiline tips
|
||||
// have the same indent on the left-hand side:
|
||||
let tip_indent = " ".repeat(" tip: ".len());
|
||||
|
||||
let mut tip = format!(
|
||||
"\
|
||||
A `--config` flag must either be a path to a `.toml` configuration file
|
||||
{tip_indent}or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
|
||||
{tip_indent}option"
|
||||
);
|
||||
|
||||
// Here we do some heuristics to try to figure out whether
|
||||
// the user was trying to pass in a path to a configuration file
|
||||
// or some inline TOML.
|
||||
// We want to display the most helpful error to the user as possible.
|
||||
if std::path::Path::new(value)
|
||||
.extension()
|
||||
.map_or(false, |ext| ext.eq_ignore_ascii_case("toml"))
|
||||
{
|
||||
if !value.contains('=') {
|
||||
tip.push_str(&format!(
|
||||
"
|
||||
|
||||
It looks like you were trying to pass a path to a configuration file.
|
||||
The path `{value}` does not exist"
|
||||
));
|
||||
}
|
||||
} else if value.contains('=') {
|
||||
tip.push_str(&format!("\n\n{toml_parse_error}"));
|
||||
}
|
||||
|
||||
new_error.insert(
|
||||
clap::error::ContextKind::Suggested,
|
||||
clap::error::ContextValue::StyledStrs(vec![tip.into()]),
|
||||
);
|
||||
|
||||
Err(new_error)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_output_format(
|
||||
output_format: Option<SerializationFormat>,
|
||||
show_sources: Option<bool>,
|
||||
@@ -664,7 +935,6 @@ fn resolve_output_format(
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct CheckArguments {
|
||||
pub add_noqa: bool,
|
||||
pub config: Option<PathBuf>,
|
||||
pub diff: bool,
|
||||
pub ecosystem_ci: bool,
|
||||
pub exit_non_zero_on_fix: bool,
|
||||
@@ -688,7 +958,6 @@ pub struct FormatArguments {
|
||||
pub check: bool,
|
||||
pub no_cache: bool,
|
||||
pub diff: bool,
|
||||
pub config: Option<PathBuf>,
|
||||
pub files: Vec<PathBuf>,
|
||||
pub isolated: bool,
|
||||
pub stdin_filename: Option<PathBuf>,
|
||||
@@ -884,39 +1153,40 @@ impl LineColumnParseError {
|
||||
}
|
||||
}
|
||||
|
||||
/// CLI settings that function as configuration overrides.
|
||||
/// Configuration overrides provided via dedicated CLI flags:
|
||||
/// `--line-length`, `--respect-gitignore`, etc.
|
||||
#[derive(Clone, Default)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct CliOverrides {
|
||||
pub dummy_variable_rgx: Option<Regex>,
|
||||
pub exclude: Option<Vec<FilePattern>>,
|
||||
pub extend_exclude: Option<Vec<FilePattern>>,
|
||||
pub extend_fixable: Option<Vec<RuleSelector>>,
|
||||
pub extend_ignore: Option<Vec<RuleSelector>>,
|
||||
pub extend_select: Option<Vec<RuleSelector>>,
|
||||
pub extend_unfixable: Option<Vec<RuleSelector>>,
|
||||
pub fixable: Option<Vec<RuleSelector>>,
|
||||
pub ignore: Option<Vec<RuleSelector>>,
|
||||
pub line_length: Option<LineLength>,
|
||||
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
|
||||
pub extend_per_file_ignores: Option<Vec<PatternPrefixPair>>,
|
||||
pub preview: Option<PreviewMode>,
|
||||
pub respect_gitignore: Option<bool>,
|
||||
pub select: Option<Vec<RuleSelector>>,
|
||||
pub target_version: Option<PythonVersion>,
|
||||
pub unfixable: Option<Vec<RuleSelector>>,
|
||||
struct ExplicitConfigOverrides {
|
||||
dummy_variable_rgx: Option<Regex>,
|
||||
exclude: Option<Vec<FilePattern>>,
|
||||
extend_exclude: Option<Vec<FilePattern>>,
|
||||
extend_fixable: Option<Vec<RuleSelector>>,
|
||||
extend_ignore: Option<Vec<RuleSelector>>,
|
||||
extend_select: Option<Vec<RuleSelector>>,
|
||||
extend_unfixable: Option<Vec<RuleSelector>>,
|
||||
fixable: Option<Vec<RuleSelector>>,
|
||||
ignore: Option<Vec<RuleSelector>>,
|
||||
line_length: Option<LineLength>,
|
||||
per_file_ignores: Option<Vec<PatternPrefixPair>>,
|
||||
extend_per_file_ignores: Option<Vec<PatternPrefixPair>>,
|
||||
preview: Option<PreviewMode>,
|
||||
respect_gitignore: Option<bool>,
|
||||
select: Option<Vec<RuleSelector>>,
|
||||
target_version: Option<PythonVersion>,
|
||||
unfixable: Option<Vec<RuleSelector>>,
|
||||
// TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`.
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
pub fix: Option<bool>,
|
||||
pub fix_only: Option<bool>,
|
||||
pub unsafe_fixes: Option<UnsafeFixes>,
|
||||
pub force_exclude: Option<bool>,
|
||||
pub output_format: Option<SerializationFormat>,
|
||||
pub show_fixes: Option<bool>,
|
||||
pub extension: Option<Vec<ExtensionPair>>,
|
||||
cache_dir: Option<PathBuf>,
|
||||
fix: Option<bool>,
|
||||
fix_only: Option<bool>,
|
||||
unsafe_fixes: Option<UnsafeFixes>,
|
||||
force_exclude: Option<bool>,
|
||||
output_format: Option<SerializationFormat>,
|
||||
show_fixes: Option<bool>,
|
||||
extension: Option<Vec<ExtensionPair>>,
|
||||
}
|
||||
|
||||
impl ConfigurationTransformer for CliOverrides {
|
||||
impl ConfigurationTransformer for ExplicitConfigOverrides {
|
||||
fn transform(&self, mut config: Configuration) -> Configuration {
|
||||
if let Some(cache_dir) = &self.cache_dir {
|
||||
config.cache_dir = Some(cache_dir.clone());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::fmt::Debug;
|
||||
use std::fs::{self, File};
|
||||
use std::hash::Hasher;
|
||||
use std::io::{self, BufReader, BufWriter, Write};
|
||||
use std::io::{self, BufReader, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Mutex;
|
||||
@@ -15,6 +15,7 @@ use rayon::iter::ParallelIterator;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelBridge};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use ruff_cache::{CacheKey, CacheKeyHasher};
|
||||
use ruff_diagnostics::{DiagnosticKind, Fix};
|
||||
@@ -165,15 +166,29 @@ impl Cache {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let file = File::create(&self.path)
|
||||
.with_context(|| format!("Failed to create cache file '{}'", self.path.display()))?;
|
||||
let writer = BufWriter::new(file);
|
||||
bincode::serialize_into(writer, &self.package).with_context(|| {
|
||||
// Write the cache to a temporary file first and then rename it for an "atomic" write.
|
||||
// Protects against data loss if the process is killed during the write and races between different ruff
|
||||
// processes, resulting in a corrupted cache file. https://github.com/astral-sh/ruff/issues/8147#issuecomment-1943345964
|
||||
let mut temp_file =
|
||||
NamedTempFile::new_in(self.path.parent().expect("Write path must have a parent"))
|
||||
.context("Failed to create temporary file")?;
|
||||
|
||||
// Serialize to in-memory buffer because hyperfine benchmark showed that it's faster than
|
||||
// using a `BufWriter` and our cache files are small enough that streaming isn't necessary.
|
||||
let serialized =
|
||||
bincode::serialize(&self.package).context("Failed to serialize cache data")?;
|
||||
temp_file
|
||||
.write_all(&serialized)
|
||||
.context("Failed to write serialized cache to temporary file.")?;
|
||||
|
||||
temp_file.persist(&self.path).with_context(|| {
|
||||
format!(
|
||||
"Failed to serialise cache to file '{}'",
|
||||
"Failed to rename temporary cache file to {}",
|
||||
self.path.display()
|
||||
)
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Applies the pending changes without storing the cache to disk.
|
||||
|
||||
@@ -12,17 +12,17 @@ use ruff_linter::warn_user_once;
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
use crate::args::ConfigArguments;
|
||||
|
||||
/// Add `noqa` directives to a collection of files.
|
||||
pub(crate) fn add_noqa(
|
||||
files: &[PathBuf],
|
||||
pyproject_config: &PyprojectConfig,
|
||||
overrides: &CliOverrides,
|
||||
config_arguments: &ConfigArguments,
|
||||
) -> Result<usize> {
|
||||
// Collect all the files to check.
|
||||
let start = Instant::now();
|
||||
let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
|
||||
let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?;
|
||||
let duration = start.elapsed();
|
||||
debug!("Identified files to lint in: {:?}", duration);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use ruff_workspace::resolver::{
|
||||
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile,
|
||||
};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
use crate::args::ConfigArguments;
|
||||
use crate::cache::{Cache, PackageCacheMap, PackageCaches};
|
||||
use crate::diagnostics::Diagnostics;
|
||||
use crate::panic::catch_unwind;
|
||||
@@ -34,7 +34,7 @@ use crate::panic::catch_unwind;
|
||||
pub(crate) fn check(
|
||||
files: &[PathBuf],
|
||||
pyproject_config: &PyprojectConfig,
|
||||
overrides: &CliOverrides,
|
||||
config_arguments: &ConfigArguments,
|
||||
cache: flags::Cache,
|
||||
noqa: flags::Noqa,
|
||||
fix_mode: flags::FixMode,
|
||||
@@ -42,7 +42,7 @@ pub(crate) fn check(
|
||||
) -> Result<Diagnostics> {
|
||||
// Collect all the Python files to check.
|
||||
let start = Instant::now();
|
||||
let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
|
||||
let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?;
|
||||
debug!("Identified files to lint in: {:?}", start.elapsed());
|
||||
|
||||
if paths.is_empty() {
|
||||
@@ -233,7 +233,7 @@ mod test {
|
||||
use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy};
|
||||
use ruff_workspace::Settings;
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
use crate::args::ConfigArguments;
|
||||
|
||||
use super::check;
|
||||
|
||||
@@ -272,7 +272,7 @@ mod test {
|
||||
// Notebooks are not included by default
|
||||
&[tempdir.path().to_path_buf(), notebook],
|
||||
&pyproject_config,
|
||||
&CliOverrides::default(),
|
||||
&ConfigArguments::default(),
|
||||
flags::Cache::Disabled,
|
||||
flags::Noqa::Disabled,
|
||||
flags::FixMode::Generate,
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_linter::packaging;
|
||||
use ruff_linter::settings::flags;
|
||||
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig, Resolver};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
use crate::args::ConfigArguments;
|
||||
use crate::diagnostics::{lint_stdin, Diagnostics};
|
||||
use crate::stdin::{parrot_stdin, read_from_stdin};
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::stdin::{parrot_stdin, read_from_stdin};
|
||||
pub(crate) fn check_stdin(
|
||||
filename: Option<&Path>,
|
||||
pyproject_config: &PyprojectConfig,
|
||||
overrides: &CliOverrides,
|
||||
overrides: &ConfigArguments,
|
||||
noqa: flags::Noqa,
|
||||
fix_mode: flags::FixMode,
|
||||
) -> Result<Diagnostics> {
|
||||
|
||||
@@ -29,7 +29,7 @@ use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments, FormatRange};
|
||||
use crate::args::{ConfigArguments, FormatArguments, FormatRange};
|
||||
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
|
||||
use crate::panic::{catch_unwind, PanicError};
|
||||
use crate::resolve::resolve;
|
||||
@@ -60,18 +60,17 @@ impl FormatMode {
|
||||
/// Format a set of files, and return the exit status.
|
||||
pub(crate) fn format(
|
||||
cli: FormatArguments,
|
||||
overrides: &CliOverrides,
|
||||
config_arguments: &ConfigArguments,
|
||||
log_level: LogLevel,
|
||||
) -> Result<ExitStatus> {
|
||||
let pyproject_config = resolve(
|
||||
cli.isolated,
|
||||
cli.config.as_deref(),
|
||||
overrides,
|
||||
config_arguments,
|
||||
cli.stdin_filename.as_deref(),
|
||||
)?;
|
||||
let mode = FormatMode::from_cli(&cli);
|
||||
let files = resolve_default_files(cli.files, false);
|
||||
let (paths, resolver) = python_files_in_path(&files, &pyproject_config, overrides)?;
|
||||
let (paths, resolver) = python_files_in_path(&files, &pyproject_config, config_arguments)?;
|
||||
|
||||
if paths.is_empty() {
|
||||
warn_user_once!("No Python files found under the given path(s)");
|
||||
|
||||
@@ -9,7 +9,7 @@ use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments, FormatRange};
|
||||
use crate::args::{ConfigArguments, FormatArguments, FormatRange};
|
||||
use crate::commands::format::{
|
||||
format_source, warn_incompatible_formatter_settings, FormatCommandError, FormatMode,
|
||||
FormatResult, FormattedSource,
|
||||
@@ -19,11 +19,13 @@ use crate::stdin::{parrot_stdin, read_from_stdin};
|
||||
use crate::ExitStatus;
|
||||
|
||||
/// Run the formatter over a single file, read from `stdin`.
|
||||
pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> Result<ExitStatus> {
|
||||
pub(crate) fn format_stdin(
|
||||
cli: &FormatArguments,
|
||||
config_arguments: &ConfigArguments,
|
||||
) -> Result<ExitStatus> {
|
||||
let pyproject_config = resolve(
|
||||
cli.isolated,
|
||||
cli.config.as_deref(),
|
||||
overrides,
|
||||
config_arguments,
|
||||
cli.stdin_filename.as_deref(),
|
||||
)?;
|
||||
|
||||
@@ -34,7 +36,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||
|
||||
if resolver.force_exclude() {
|
||||
if let Some(filename) = cli.stdin_filename.as_deref() {
|
||||
if !python_file_at_path(filename, &mut resolver, overrides)? {
|
||||
if !python_file_at_path(filename, &mut resolver, config_arguments)? {
|
||||
if mode.is_write() {
|
||||
parrot_stdin()?;
|
||||
}
|
||||
|
||||
@@ -7,17 +7,17 @@ use itertools::Itertools;
|
||||
use ruff_linter::warn_user_once;
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
use crate::args::ConfigArguments;
|
||||
|
||||
/// Show the list of files to be checked based on current settings.
|
||||
pub(crate) fn show_files(
|
||||
files: &[PathBuf],
|
||||
pyproject_config: &PyprojectConfig,
|
||||
overrides: &CliOverrides,
|
||||
config_arguments: &ConfigArguments,
|
||||
writer: &mut impl Write,
|
||||
) -> Result<()> {
|
||||
// Collect all files in the hierarchy.
|
||||
let (paths, _resolver) = python_files_in_path(files, pyproject_config, overrides)?;
|
||||
let (paths, _resolver) = python_files_in_path(files, pyproject_config, config_arguments)?;
|
||||
|
||||
if paths.is_empty() {
|
||||
warn_user_once!("No Python files found under the given path(s)");
|
||||
|
||||
@@ -6,17 +6,17 @@ use itertools::Itertools;
|
||||
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
use crate::args::ConfigArguments;
|
||||
|
||||
/// Print the user-facing configuration settings.
|
||||
pub(crate) fn show_settings(
|
||||
files: &[PathBuf],
|
||||
pyproject_config: &PyprojectConfig,
|
||||
overrides: &CliOverrides,
|
||||
config_arguments: &ConfigArguments,
|
||||
writer: &mut impl Write,
|
||||
) -> Result<()> {
|
||||
// Collect all files in the hierarchy.
|
||||
let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
|
||||
let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?;
|
||||
|
||||
// Print the list of files.
|
||||
let Some(path) = paths
|
||||
@@ -31,9 +31,9 @@ pub(crate) fn show_settings(
|
||||
|
||||
let settings = resolver.resolve(&path);
|
||||
|
||||
writeln!(writer, "Resolved settings for: {path:?}")?;
|
||||
writeln!(writer, "Resolved settings for: \"{}\"", path.display())?;
|
||||
if let Some(settings_path) = pyproject_config.path.as_ref() {
|
||||
writeln!(writer, "Settings path: {settings_path:?}")?;
|
||||
writeln!(writer, "Settings path: \"{}\"", settings_path.display())?;
|
||||
}
|
||||
write!(writer, "{settings}")?;
|
||||
|
||||
|
||||
@@ -204,24 +204,23 @@ pub fn run(
|
||||
}
|
||||
|
||||
fn format(args: FormatCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
let (cli, overrides) = args.partition();
|
||||
let (cli, config_arguments) = args.partition()?;
|
||||
|
||||
if is_stdin(&cli.files, cli.stdin_filename.as_deref()) {
|
||||
commands::format_stdin::format_stdin(&cli, &overrides)
|
||||
commands::format_stdin::format_stdin(&cli, &config_arguments)
|
||||
} else {
|
||||
commands::format::format(cli, &overrides, log_level)
|
||||
commands::format::format(cli, &config_arguments, log_level)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
let (cli, overrides) = args.partition();
|
||||
let (cli, config_arguments) = args.partition()?;
|
||||
|
||||
// Construct the "default" settings. These are used when no `pyproject.toml`
|
||||
// files are present, or files are injected from outside of the hierarchy.
|
||||
let pyproject_config = resolve::resolve(
|
||||
cli.isolated,
|
||||
cli.config.as_deref(),
|
||||
&overrides,
|
||||
&config_arguments,
|
||||
cli.stdin_filename.as_deref(),
|
||||
)?;
|
||||
|
||||
@@ -239,11 +238,21 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
let files = resolve_default_files(cli.files, is_stdin);
|
||||
|
||||
if cli.show_settings {
|
||||
commands::show_settings::show_settings(&files, &pyproject_config, &overrides, &mut writer)?;
|
||||
commands::show_settings::show_settings(
|
||||
&files,
|
||||
&pyproject_config,
|
||||
&config_arguments,
|
||||
&mut writer,
|
||||
)?;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
if cli.show_files {
|
||||
commands::show_files::show_files(&files, &pyproject_config, &overrides, &mut writer)?;
|
||||
commands::show_files::show_files(
|
||||
&files,
|
||||
&pyproject_config,
|
||||
&config_arguments,
|
||||
&mut writer,
|
||||
)?;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
@@ -302,7 +311,8 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
if !fix_mode.is_generate() {
|
||||
warn_user!("--fix is incompatible with --add-noqa.");
|
||||
}
|
||||
let modifications = commands::add_noqa::add_noqa(&files, &pyproject_config, &overrides)?;
|
||||
let modifications =
|
||||
commands::add_noqa::add_noqa(&files, &pyproject_config, &config_arguments)?;
|
||||
if modifications > 0 && log_level >= LogLevel::Default {
|
||||
let s = if modifications == 1 { "" } else { "s" };
|
||||
#[allow(clippy::print_stderr)]
|
||||
@@ -352,7 +362,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
let messages = commands::check::check(
|
||||
&files,
|
||||
&pyproject_config,
|
||||
&overrides,
|
||||
&config_arguments,
|
||||
cache.into(),
|
||||
noqa.into(),
|
||||
fix_mode,
|
||||
@@ -374,8 +384,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
if matches!(change_kind, ChangeKind::Configuration) {
|
||||
pyproject_config = resolve::resolve(
|
||||
cli.isolated,
|
||||
cli.config.as_deref(),
|
||||
&overrides,
|
||||
&config_arguments,
|
||||
cli.stdin_filename.as_deref(),
|
||||
)?;
|
||||
}
|
||||
@@ -385,7 +394,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
let messages = commands::check::check(
|
||||
&files,
|
||||
&pyproject_config,
|
||||
&overrides,
|
||||
&config_arguments,
|
||||
cache.into(),
|
||||
noqa.into(),
|
||||
fix_mode,
|
||||
@@ -402,7 +411,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
commands::check_stdin::check_stdin(
|
||||
cli.stdin_filename.map(fs::normalize_path).as_deref(),
|
||||
&pyproject_config,
|
||||
&overrides,
|
||||
&config_arguments,
|
||||
noqa.into(),
|
||||
fix_mode,
|
||||
)?
|
||||
@@ -410,7 +419,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
commands::check::check(
|
||||
&files,
|
||||
&pyproject_config,
|
||||
&overrides,
|
||||
&config_arguments,
|
||||
cache.into(),
|
||||
noqa.into(),
|
||||
fix_mode,
|
||||
|
||||
@@ -11,19 +11,18 @@ use ruff_workspace::resolver::{
|
||||
Relativity,
|
||||
};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
use crate::args::ConfigArguments;
|
||||
|
||||
/// Resolve the relevant settings strategy and defaults for the current
|
||||
/// invocation.
|
||||
pub fn resolve(
|
||||
isolated: bool,
|
||||
config: Option<&Path>,
|
||||
overrides: &CliOverrides,
|
||||
config_arguments: &ConfigArguments,
|
||||
stdin_filename: Option<&Path>,
|
||||
) -> Result<PyprojectConfig> {
|
||||
// First priority: if we're running in isolated mode, use the default settings.
|
||||
if isolated {
|
||||
let config = overrides.transform(Configuration::default());
|
||||
let config = config_arguments.transform(Configuration::default());
|
||||
let settings = config.into_settings(&path_dedot::CWD)?;
|
||||
debug!("Isolated mode, not reading any pyproject.toml");
|
||||
return Ok(PyprojectConfig::new(
|
||||
@@ -36,12 +35,13 @@ pub fn resolve(
|
||||
// Second priority: the user specified a `pyproject.toml` file. Use that
|
||||
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
|
||||
// current working directory. (This matches ESLint's behavior.)
|
||||
if let Some(pyproject) = config
|
||||
if let Some(pyproject) = config_arguments
|
||||
.config_file()
|
||||
.map(|config| config.display().to_string())
|
||||
.map(|config| shellexpand::full(&config).map(|config| PathBuf::from(config.as_ref())))
|
||||
.transpose()?
|
||||
{
|
||||
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, overrides)?;
|
||||
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, config_arguments)?;
|
||||
debug!(
|
||||
"Using user-specified configuration file at: {}",
|
||||
pyproject.display()
|
||||
@@ -67,7 +67,7 @@ pub fn resolve(
|
||||
"Using configuration file (via parent) at: {}",
|
||||
pyproject.display()
|
||||
);
|
||||
let settings = resolve_root_settings(&pyproject, Relativity::Parent, overrides)?;
|
||||
let settings = resolve_root_settings(&pyproject, Relativity::Parent, config_arguments)?;
|
||||
return Ok(PyprojectConfig::new(
|
||||
PyprojectDiscoveryStrategy::Hierarchical,
|
||||
settings,
|
||||
@@ -84,7 +84,7 @@ pub fn resolve(
|
||||
"Using configuration file (via cwd) at: {}",
|
||||
pyproject.display()
|
||||
);
|
||||
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, overrides)?;
|
||||
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, config_arguments)?;
|
||||
return Ok(PyprojectConfig::new(
|
||||
PyprojectDiscoveryStrategy::Hierarchical,
|
||||
settings,
|
||||
@@ -97,7 +97,7 @@ pub fn resolve(
|
||||
// "closest" `pyproject.toml` file for every Python file later on, so these act
|
||||
// as the "default" settings.)
|
||||
debug!("Using Ruff default settings");
|
||||
let config = overrides.transform(Configuration::default());
|
||||
let config = config_arguments.transform(Configuration::default());
|
||||
let settings = config.into_settings(&path_dedot::CWD)?;
|
||||
Ok(PyprojectConfig::new(
|
||||
PyprojectDiscoveryStrategy::Hierarchical,
|
||||
|
||||
@@ -90,6 +90,179 @@ fn format_warn_stdin_filename_with_files() {
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonexistent_config_file() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--config", "foo.toml", "."]), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value 'foo.toml' for '--config <CONFIG_OPTION>'
|
||||
|
||||
tip: A `--config` flag must either be a path to a `.toml` configuration file
|
||||
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
|
||||
option
|
||||
|
||||
It looks like you were trying to pass a path to a configuration file.
|
||||
The path `foo.toml` does not exist
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_override_rejected_if_invalid_toml() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--config", "foo = bar", "."]), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value 'foo = bar' for '--config <CONFIG_OPTION>'
|
||||
|
||||
tip: A `--config` flag must either be a path to a `.toml` configuration file
|
||||
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
|
||||
option
|
||||
|
||||
The supplied argument is not valid TOML:
|
||||
|
||||
TOML parse error at line 1, column 7
|
||||
|
|
||||
1 | foo = bar
|
||||
| ^
|
||||
invalid string
|
||||
expected `"`, `'`
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_many_config_files() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_dot_toml = tempdir.path().join("ruff.toml");
|
||||
let ruff2_dot_toml = tempdir.path().join("ruff2.toml");
|
||||
fs::File::create(&ruff_dot_toml)?;
|
||||
fs::File::create(&ruff2_dot_toml)?;
|
||||
let expected_stderr = format!(
|
||||
"\
|
||||
ruff failed
|
||||
Cause: You cannot specify more than one configuration file on the command line.
|
||||
|
||||
tip: remove either `--config={}` or `--config={}`.
|
||||
For more information, try `--help`.
|
||||
|
||||
",
|
||||
ruff_dot_toml.display(),
|
||||
ruff2_dot_toml.display(),
|
||||
);
|
||||
let cmd = Command::new(get_cargo_bin(BIN_NAME))
|
||||
.arg("format")
|
||||
.arg("--config")
|
||||
.arg(&ruff_dot_toml)
|
||||
.arg("--config")
|
||||
.arg(&ruff2_dot_toml)
|
||||
.arg(".")
|
||||
.output()?;
|
||||
let stderr = std::str::from_utf8(&cmd.stderr)?;
|
||||
assert_eq!(stderr, expected_stderr);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_and_isolated() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_dot_toml = tempdir.path().join("ruff.toml");
|
||||
fs::File::create(&ruff_dot_toml)?;
|
||||
let expected_stderr = format!(
|
||||
"\
|
||||
ruff failed
|
||||
Cause: The argument `--config={}` cannot be used with `--isolated`
|
||||
|
||||
tip: You cannot specify a configuration file and also specify `--isolated`,
|
||||
as `--isolated` causes ruff to ignore all configuration files.
|
||||
For more information, try `--help`.
|
||||
|
||||
",
|
||||
ruff_dot_toml.display(),
|
||||
);
|
||||
let cmd = Command::new(get_cargo_bin(BIN_NAME))
|
||||
.arg("format")
|
||||
.arg("--config")
|
||||
.arg(&ruff_dot_toml)
|
||||
.arg("--isolated")
|
||||
.arg(".")
|
||||
.output()?;
|
||||
let stderr = std::str::from_utf8(&cmd.stderr)?;
|
||||
assert_eq!(stderr, expected_stderr);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_override_via_cli() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(&ruff_toml, "line-length = 100")?;
|
||||
let fixture = r#"
|
||||
def foo():
|
||||
print("looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong string")
|
||||
|
||||
"#;
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.arg("format")
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
// This overrides the long line length set in the config file
|
||||
.args(["--config", "line-length=80"])
|
||||
.arg("-")
|
||||
.pass_stdin(fixture), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
def foo():
|
||||
print(
|
||||
"looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong string"
|
||||
)
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_doubly_overridden_via_cli() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(&ruff_toml, "line-length = 70")?;
|
||||
let fixture = r#"
|
||||
def foo():
|
||||
print("looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong string")
|
||||
|
||||
"#;
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.arg("format")
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
// This overrides the long line length set in the config file...
|
||||
.args(["--config", "line-length=80"])
|
||||
// ...but this overrides them both:
|
||||
.args(["--line-length", "100"])
|
||||
.arg("-")
|
||||
.pass_stdin(fixture), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
def foo():
|
||||
print("looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong string")
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_options() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
@@ -510,6 +510,341 @@ ignore = ["D203", "D212"]
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonexistent_config_file() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--config", "foo.toml", "."]), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value 'foo.toml' for '--config <CONFIG_OPTION>'
|
||||
|
||||
tip: A `--config` flag must either be a path to a `.toml` configuration file
|
||||
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
|
||||
option
|
||||
|
||||
It looks like you were trying to pass a path to a configuration file.
|
||||
The path `foo.toml` does not exist
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_override_rejected_if_invalid_toml() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--config", "foo = bar", "."]), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value 'foo = bar' for '--config <CONFIG_OPTION>'
|
||||
|
||||
tip: A `--config` flag must either be a path to a `.toml` configuration file
|
||||
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
|
||||
option
|
||||
|
||||
The supplied argument is not valid TOML:
|
||||
|
||||
TOML parse error at line 1, column 7
|
||||
|
|
||||
1 | foo = bar
|
||||
| ^
|
||||
invalid string
|
||||
expected `"`, `'`
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_many_config_files() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_dot_toml = tempdir.path().join("ruff.toml");
|
||||
let ruff2_dot_toml = tempdir.path().join("ruff2.toml");
|
||||
fs::File::create(&ruff_dot_toml)?;
|
||||
fs::File::create(&ruff2_dot_toml)?;
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_dot_toml)
|
||||
.arg("--config")
|
||||
.arg(&ruff2_dot_toml)
|
||||
.arg("."), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: You cannot specify more than one configuration file on the command line.
|
||||
|
||||
tip: remove either `--config=[TMP]/ruff.toml` or `--config=[TMP]/ruff2.toml`.
|
||||
For more information, try `--help`.
|
||||
|
||||
"###);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_and_isolated() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_dot_toml = tempdir.path().join("ruff.toml");
|
||||
fs::File::create(&ruff_dot_toml)?;
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_dot_toml)
|
||||
.arg("--isolated")
|
||||
.arg("."), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: The argument `--config=[TMP]/ruff.toml` cannot be used with `--isolated`
|
||||
|
||||
tip: You cannot specify a configuration file and also specify `--isolated`,
|
||||
as `--isolated` causes ruff to ignore all configuration files.
|
||||
For more information, try `--help`.
|
||||
|
||||
"###);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_override_via_cli() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
line-length = 100
|
||||
|
||||
[lint]
|
||||
select = ["I"]
|
||||
|
||||
[lint.isort]
|
||||
combine-as-imports = true
|
||||
"#,
|
||||
)?;
|
||||
let fixture = r#"
|
||||
from foo import (
|
||||
aaaaaaaaaaaaaaaaaaa,
|
||||
bbbbbbbbbbb as bbbbbbbbbbbbbbbb,
|
||||
cccccccccccccccc,
|
||||
ddddddddddd as ddddddddddddd,
|
||||
eeeeeeeeeeeeeee,
|
||||
ffffffffffff as ffffffffffffff,
|
||||
ggggggggggggg,
|
||||
hhhhhhh as hhhhhhhhhhh,
|
||||
iiiiiiiiiiiiii,
|
||||
jjjjjjjjjjjjj as jjjjjj,
|
||||
)
|
||||
|
||||
x = "longer_than_90_charactersssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss"
|
||||
"#;
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
.args(["--config", "line-length=90"])
|
||||
.args(["--config", "lint.extend-select=['E501', 'F841']"])
|
||||
.args(["--config", "lint.isort.combine-as-imports = false"])
|
||||
.arg("-")
|
||||
.pass_stdin(fixture), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:2:1: I001 [*] Import block is un-sorted or un-formatted
|
||||
-:15:91: E501 Line too long (97 > 90)
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_toml_but_nonexistent_option_provided_via_config_argument() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args([".", "--config", "extend-select=['F481']"]), // No such code as F481!
|
||||
@r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value 'extend-select=['F481']' for '--config <CONFIG_OPTION>'
|
||||
|
||||
tip: A `--config` flag must either be a path to a `.toml` configuration file
|
||||
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
|
||||
option
|
||||
|
||||
Could not parse the supplied argument as a `ruff.toml` configuration option:
|
||||
|
||||
Unknown rule selector: `F481`
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_toml_option_requires_a_new_flag_1() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
// commas can't be used to delimit different config overrides;
|
||||
// you need a new --config flag for each override
|
||||
.args([".", "--config", "extend-select=['F841'], line-length=90"]),
|
||||
@r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value 'extend-select=['F841'], line-length=90' for '--config <CONFIG_OPTION>'
|
||||
|
||||
tip: A `--config` flag must either be a path to a `.toml` configuration file
|
||||
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
|
||||
option
|
||||
|
||||
The supplied argument is not valid TOML:
|
||||
|
||||
TOML parse error at line 1, column 23
|
||||
|
|
||||
1 | extend-select=['F841'], line-length=90
|
||||
| ^
|
||||
expected newline, `#`
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_toml_option_requires_a_new_flag_2() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
// spaces *also* can't be used to delimit different config overrides;
|
||||
// you need a new --config flag for each override
|
||||
.args([".", "--config", "extend-select=['F841'] line-length=90"]),
|
||||
@r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value 'extend-select=['F841'] line-length=90' for '--config <CONFIG_OPTION>'
|
||||
|
||||
tip: A `--config` flag must either be a path to a `.toml` configuration file
|
||||
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
|
||||
option
|
||||
|
||||
The supplied argument is not valid TOML:
|
||||
|
||||
TOML parse error at line 1, column 24
|
||||
|
|
||||
1 | extend-select=['F841'] line-length=90
|
||||
| ^
|
||||
expected newline, `#`
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_doubly_overridden_via_cli() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
line-length = 100
|
||||
|
||||
[lint]
|
||||
select=["E501"]
|
||||
"#,
|
||||
)?;
|
||||
let fixture = "x = 'longer_than_90_charactersssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss'";
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
// The --line-length flag takes priority over both the config file
|
||||
// and the `--config="line-length=110"` flag,
|
||||
// despite them both being specified after this flag on the command line:
|
||||
.args(["--line-length", "90"])
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
.args(["--config", "line-length=110"])
|
||||
.arg("-")
|
||||
.pass_stdin(fixture), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:91: E501 Line too long (97 > 90)
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_config_setting_overridden_via_cli() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(&ruff_toml, "lint.select = ['N801']")?;
|
||||
let fixture = "class violates_n801: pass";
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
.args(["--config", "lint.per-file-ignores = {'generated.py' = ['N801']}"])
|
||||
.args(["--stdin-filename", "generated.py"])
|
||||
.arg("-")
|
||||
.pass_stdin(fixture), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deprecated_config_option_overridden_via_cli() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--config", "select=['N801']", "-"])
|
||||
.pass_stdin("class lowercase: ..."),
|
||||
@r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:7: N801 Class name `lowercase` should use CapWords convention
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your `--config` CLI arguments:
|
||||
- 'select' -> 'lint.select'
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
@@ -4,25 +4,29 @@ use std::process::Command;
|
||||
|
||||
const BIN_NAME: &str = "ruff";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const TEST_FILTERS: &[(&str, &str)] = &[
|
||||
("\"[^\\*\"]*/pyproject.toml", "\"[BASEPATH]/pyproject.toml"),
|
||||
("\".*/crates", "\"[BASEPATH]/crates"),
|
||||
("\".*/\\.ruff_cache", "\"[BASEPATH]/.ruff_cache"),
|
||||
("\".*/ruff\"", "\"[BASEPATH]\""),
|
||||
];
|
||||
#[cfg(target_os = "windows")]
|
||||
const TEST_FILTERS: &[(&str, &str)] = &[
|
||||
(r#""[^\*"]*\\pyproject.toml"#, "\"[BASEPATH]/pyproject.toml"),
|
||||
(r#"".*\\crates"#, "\"[BASEPATH]/crates"),
|
||||
(r#"".*\\\.ruff_cache"#, "\"[BASEPATH]/.ruff_cache"),
|
||||
(r#"".*\\ruff""#, "\"[BASEPATH]\""),
|
||||
(r#"\\+(\w\w|\s|")"#, "/$1"),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn display_default_settings() {
|
||||
insta::with_settings!({ filters => TEST_FILTERS.to_vec() }, {
|
||||
// Navigate from the crate directory to the workspace root.
|
||||
let base_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap();
|
||||
let base_path = base_path.to_string_lossy();
|
||||
|
||||
// Escape the backslashes for the regex.
|
||||
let base_path = regex::escape(&base_path);
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let test_filters = &[(base_path.as_ref(), "[BASEPATH]")];
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let test_filters = &[
|
||||
(base_path.as_ref(), "[BASEPATH]"),
|
||||
(r#"\\+(\w\w|\s|\.|")"#, "/$1"),
|
||||
];
|
||||
|
||||
insta::with_settings!({ filters => test_filters.to_vec() }, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["check", "--show-settings", "unformatted.py"]).current_dir(Path::new("./resources/test/fixtures")));
|
||||
});
|
||||
|
||||
@@ -25,6 +25,15 @@ import cycles. They also increase the cognitive load of reading the code.
|
||||
If an import statement is used to check for the availability or existence
|
||||
of a module, consider using `importlib.util.find_spec` instead.
|
||||
|
||||
If an import statement is used to re-export a symbol as part of a module's
|
||||
public interface, consider using a "redundant" import alias, which
|
||||
instructs Ruff (and other tools) to respect the re-export, and avoid
|
||||
marking it as unused, as in:
|
||||
|
||||
```python
|
||||
from module import member as member
|
||||
```
|
||||
|
||||
## Example
|
||||
```python
|
||||
import numpy as np # unused import
|
||||
@@ -51,11 +60,12 @@ else:
|
||||
```
|
||||
|
||||
## Options
|
||||
- `lint.pyflakes.extend-generics`
|
||||
- `lint.ignore-init-module-imports`
|
||||
|
||||
## References
|
||||
- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)
|
||||
- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)
|
||||
- [Typing documentation: interface conventions](https://typing.readthedocs.io/en/latest/source/libraries.html#library-interface-public-and-private-symbols)
|
||||
|
||||
----- stderr -----
|
||||
|
||||
|
||||
@@ -205,7 +205,9 @@ linter.external = []
|
||||
linter.ignore_init_module_imports = false
|
||||
linter.logger_objects = []
|
||||
linter.namespace_packages = []
|
||||
linter.src = ["[BASEPATH]"]
|
||||
linter.src = [
|
||||
"[BASEPATH]",
|
||||
]
|
||||
linter.tab_size = 4
|
||||
linter.line_length = 88
|
||||
linter.task_tags = [
|
||||
|
||||
@@ -13,6 +13,7 @@ license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
doctest = false
|
||||
|
||||
[[bench]]
|
||||
name = "linter"
|
||||
|
||||
@@ -27,7 +27,7 @@ use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use ruff::args::{CliOverrides, FormatArguments, FormatCommand, LogLevelArgs};
|
||||
use ruff::args::{ConfigArguments, FormatArguments, FormatCommand, LogLevelArgs};
|
||||
use ruff::resolve::resolve;
|
||||
use ruff_formatter::{FormatError, LineWidth, PrintError};
|
||||
use ruff_linter::logging::LogLevel;
|
||||
@@ -38,24 +38,23 @@ use ruff_python_formatter::{
|
||||
use ruff_python_parser::ParseError;
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
|
||||
|
||||
fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, CliOverrides)> {
|
||||
fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, ConfigArguments)> {
|
||||
let args_matches = FormatCommand::command()
|
||||
.no_binary_name(true)
|
||||
.get_matches_from(dirs);
|
||||
let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?;
|
||||
let (cli, overrides) = arguments.partition();
|
||||
Ok((cli, overrides))
|
||||
let (cli, config_arguments) = arguments.partition()?;
|
||||
Ok((cli, config_arguments))
|
||||
}
|
||||
|
||||
/// Find the [`PyprojectConfig`] to use for formatting.
|
||||
fn find_pyproject_config(
|
||||
cli: &FormatArguments,
|
||||
overrides: &CliOverrides,
|
||||
config_arguments: &ConfigArguments,
|
||||
) -> anyhow::Result<PyprojectConfig> {
|
||||
let mut pyproject_config = resolve(
|
||||
cli.isolated,
|
||||
cli.config.as_deref(),
|
||||
overrides,
|
||||
config_arguments,
|
||||
cli.stdin_filename.as_deref(),
|
||||
)?;
|
||||
// We don't want to format pyproject.toml
|
||||
@@ -72,9 +71,9 @@ fn find_pyproject_config(
|
||||
fn ruff_check_paths<'a>(
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
cli: &FormatArguments,
|
||||
overrides: &CliOverrides,
|
||||
config_arguments: &ConfigArguments,
|
||||
) -> anyhow::Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver<'a>)> {
|
||||
let (paths, resolver) = python_files_in_path(&cli.files, pyproject_config, overrides)?;
|
||||
let (paths, resolver) = python_files_in_path(&cli.files, pyproject_config, config_arguments)?;
|
||||
Ok((paths, resolver))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ruff_text_size = { path = "../ruff_text_size" }
|
||||
|
||||
@@ -21,8 +21,7 @@ impl<'a> LineSuffixes<'a> {
|
||||
/// Takes all the pending line suffixes.
|
||||
pub(super) fn take_pending<'l>(
|
||||
&'l mut self,
|
||||
) -> impl Iterator<Item = LineSuffixEntry<'a>> + DoubleEndedIterator + 'l + ExactSizeIterator
|
||||
{
|
||||
) -> impl DoubleEndedIterator<Item = LineSuffixEntry<'a>> + 'l + ExactSizeIterator {
|
||||
self.suffixes.drain(..)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ruff_macros = { path = "../ruff_macros" }
|
||||
|
||||
22
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S308.py
vendored
Normal file
22
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S308.py
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
def some_func():
|
||||
return mark_safe('<script>alert("evil!")</script>')
|
||||
|
||||
|
||||
@mark_safe
|
||||
def some_func():
|
||||
return '<script>alert("evil!")</script>'
|
||||
|
||||
|
||||
from django.utils.html import mark_safe
|
||||
|
||||
|
||||
def some_func():
|
||||
return mark_safe('<script>alert("evil!")</script>')
|
||||
|
||||
|
||||
@mark_safe
|
||||
def some_func():
|
||||
return '<script>alert("evil!")</script>'
|
||||
@@ -11,13 +11,25 @@ class _UnusedTypedDict2(typing.TypedDict):
|
||||
|
||||
|
||||
class _UsedTypedDict(TypedDict):
|
||||
foo: bytes
|
||||
foo: bytes
|
||||
|
||||
|
||||
class _CustomClass(_UsedTypedDict):
|
||||
bar: list[int]
|
||||
|
||||
|
||||
_UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int})
|
||||
_UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes})
|
||||
|
||||
|
||||
def uses_UsedTypedDict3(arg: _UsedTypedDict3) -> None: ...
|
||||
|
||||
|
||||
# In `.py` files, we don't flag unused definitions in class scopes (unlike in `.pyi`
|
||||
# files).
|
||||
class _CustomClass3:
|
||||
class _UnusedTypeDict4(TypedDict):
|
||||
pass
|
||||
|
||||
def method(self) -> None:
|
||||
_CustomClass3._UnusedTypeDict4()
|
||||
|
||||
@@ -35,3 +35,13 @@ _UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int})
|
||||
_UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes})
|
||||
|
||||
def uses_UsedTypedDict3(arg: _UsedTypedDict3) -> None: ...
|
||||
|
||||
|
||||
# In `.pyi` files, we flag unused definitions in class scopes as well as in the global
|
||||
# scope (unlike in `.py` files).
|
||||
class _CustomClass3:
|
||||
class _UnusedTypeDict4(TypedDict):
|
||||
pass
|
||||
|
||||
def method(self) -> None:
|
||||
_CustomClass3._UnusedTypeDict4()
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import trio
|
||||
|
||||
|
||||
async def foo():
|
||||
async def func():
|
||||
with trio.fail_after():
|
||||
...
|
||||
|
||||
async def foo():
|
||||
|
||||
async def func():
|
||||
with trio.fail_at():
|
||||
await ...
|
||||
|
||||
async def foo():
|
||||
|
||||
async def func():
|
||||
with trio.move_on_after():
|
||||
...
|
||||
|
||||
async def foo():
|
||||
|
||||
async def func():
|
||||
with trio.move_at():
|
||||
await ...
|
||||
|
||||
|
||||
async def func():
|
||||
with trio.move_at():
|
||||
async with trio.open_nursery() as nursery:
|
||||
...
|
||||
|
||||
@@ -19,8 +19,11 @@ numpy.random.seed()
|
||||
numpy.random.get_state()
|
||||
numpy.random.set_state()
|
||||
numpy.random.rand()
|
||||
numpy.random.ranf()
|
||||
numpy.random.sample()
|
||||
numpy.random.randn()
|
||||
numpy.random.randint()
|
||||
numpy.random.random()
|
||||
numpy.random.random_integers()
|
||||
numpy.random.random_sample()
|
||||
numpy.random.choice()
|
||||
@@ -35,7 +38,6 @@ numpy.random.exponential()
|
||||
numpy.random.f()
|
||||
numpy.random.gamma()
|
||||
numpy.random.geometric()
|
||||
numpy.random.get_state()
|
||||
numpy.random.gumbel()
|
||||
numpy.random.hypergeometric()
|
||||
numpy.random.laplace()
|
||||
|
||||
@@ -36,35 +36,47 @@ for i in list( # Comment
|
||||
): # PERF101
|
||||
pass
|
||||
|
||||
for i in list(foo_dict): # Ok
|
||||
for i in list(foo_dict): # OK
|
||||
pass
|
||||
|
||||
for i in list(1): # Ok
|
||||
for i in list(1): # OK
|
||||
pass
|
||||
|
||||
for i in list(foo_int): # Ok
|
||||
for i in list(foo_int): # OK
|
||||
pass
|
||||
|
||||
|
||||
import itertools
|
||||
|
||||
for i in itertools.product(foo_int): # Ok
|
||||
for i in itertools.product(foo_int): # OK
|
||||
pass
|
||||
|
||||
for i in list(foo_list): # Ok
|
||||
for i in list(foo_list): # OK
|
||||
foo_list.append(i + 1)
|
||||
|
||||
for i in list(foo_list): # PERF101
|
||||
# Make sure we match the correct list
|
||||
other_list.append(i + 1)
|
||||
|
||||
for i in list(foo_tuple): # Ok
|
||||
for i in list(foo_tuple): # OK
|
||||
foo_tuple.append(i + 1)
|
||||
|
||||
for i in list(foo_set): # Ok
|
||||
for i in list(foo_set): # OK
|
||||
foo_set.append(i + 1)
|
||||
|
||||
x, y, nested_tuple = (1, 2, (3, 4, 5))
|
||||
|
||||
for i in list(nested_tuple): # PERF101
|
||||
pass
|
||||
|
||||
for i in list(foo_list): # OK
|
||||
if True:
|
||||
foo_list.append(i + 1)
|
||||
|
||||
for i in list(foo_list): # OK
|
||||
if True:
|
||||
foo_list[i] = i + 1
|
||||
|
||||
for i in list(foo_list): # OK
|
||||
if True:
|
||||
del foo_list[i + 1]
|
||||
|
||||
848
crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.py
vendored
Normal file
848
crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.py
vendored
Normal file
@@ -0,0 +1,848 @@
|
||||
"""Fixtures for the errors E301, E302, E303, E304, E305 and E306.
|
||||
Since these errors are about new lines, each test starts with either "No error" or "# E30X".
|
||||
Each test's end is signaled by a "# end" line.
|
||||
There should be no E30X error outside of a test's bound.
|
||||
"""
|
||||
|
||||
|
||||
# No error
|
||||
class Class:
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# No error
|
||||
class Class:
|
||||
"""Docstring"""
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# No error
|
||||
def func():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# No error
|
||||
# comment
|
||||
class Class:
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# No error
|
||||
# comment
|
||||
def func():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
def foo():
|
||||
pass
|
||||
|
||||
|
||||
def bar():
|
||||
pass
|
||||
|
||||
|
||||
class Foo(object):
|
||||
pass
|
||||
|
||||
|
||||
class Bar(object):
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# No error
|
||||
class Class(object):
|
||||
|
||||
def func1():
|
||||
pass
|
||||
|
||||
def func2():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# No error
|
||||
class Class(object):
|
||||
|
||||
def func1():
|
||||
pass
|
||||
|
||||
# comment
|
||||
def func2():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# No error
|
||||
class Class:
|
||||
|
||||
def func1():
|
||||
pass
|
||||
|
||||
# comment
|
||||
def func2():
|
||||
pass
|
||||
|
||||
# This is a
|
||||
# ... multi-line comment
|
||||
|
||||
def func3():
|
||||
pass
|
||||
|
||||
|
||||
# This is a
|
||||
# ... multi-line comment
|
||||
|
||||
@decorator
|
||||
class Class:
|
||||
|
||||
def func1():
|
||||
pass
|
||||
|
||||
# comment
|
||||
|
||||
def func2():
|
||||
pass
|
||||
|
||||
@property
|
||||
def func3():
|
||||
pass
|
||||
|
||||
# end
|
||||
|
||||
|
||||
# No error
|
||||
try:
|
||||
from nonexistent import Bar
|
||||
except ImportError:
|
||||
class Bar(object):
|
||||
"""This is a Bar replacement"""
|
||||
# end
|
||||
|
||||
|
||||
# No error
|
||||
def with_feature(f):
|
||||
"""Some decorator"""
|
||||
wrapper = f
|
||||
if has_this_feature(f):
|
||||
def wrapper(*args):
|
||||
call_feature(args[0])
|
||||
return f(*args)
|
||||
return wrapper
|
||||
# end
|
||||
|
||||
|
||||
# No error
|
||||
try:
|
||||
next
|
||||
except NameError:
|
||||
def next(iterator, default):
|
||||
for item in iterator:
|
||||
return item
|
||||
return default
|
||||
# end
|
||||
|
||||
|
||||
# No error
|
||||
def fn():
|
||||
pass
|
||||
|
||||
|
||||
class Foo():
|
||||
"""Class Foo"""
|
||||
|
||||
def fn():
|
||||
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# No error
|
||||
# comment
|
||||
def c():
|
||||
pass
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
def d():
|
||||
pass
|
||||
|
||||
# This is a
|
||||
# ... multi-line comment
|
||||
|
||||
# And this one is
|
||||
# ... a second paragraph
|
||||
# ... which spans on 3 lines
|
||||
|
||||
|
||||
# Function `e` is below
|
||||
# NOTE: Hey this is a testcase
|
||||
|
||||
def e():
|
||||
pass
|
||||
|
||||
|
||||
def fn():
|
||||
print()
|
||||
|
||||
# comment
|
||||
|
||||
print()
|
||||
|
||||
print()
|
||||
|
||||
# Comment 1
|
||||
|
||||
# Comment 2
|
||||
|
||||
|
||||
# Comment 3
|
||||
|
||||
def fn2():
|
||||
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
if __name__ == '__main__':
|
||||
foo()
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
defaults = {}
|
||||
defaults.update({})
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
def foo(x):
|
||||
classification = x
|
||||
definitely = not classification
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
def bar(): pass
|
||||
def baz(): pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
def foo():
|
||||
def bar(): pass
|
||||
def baz(): pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
from typing import overload
|
||||
from typing import Union
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
@overload
|
||||
def f(x: int) -> int: ...
|
||||
@overload
|
||||
def f(x: str) -> str: ...
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
def f(x: Union[int, str]) -> Union[int, str]:
|
||||
return x
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class C(Protocol):
|
||||
@property
|
||||
def f(self) -> int: ...
|
||||
@property
|
||||
def g(self) -> str: ...
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
def f(
|
||||
a,
|
||||
):
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
if True:
|
||||
class Class:
|
||||
"""Docstring"""
|
||||
|
||||
def function(self):
|
||||
...
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
if True:
|
||||
def function(self):
|
||||
...
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
@decorator
|
||||
# comment
|
||||
@decorator
|
||||
def function():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
class Class:
|
||||
def method(self):
|
||||
if True:
|
||||
def function():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
@decorator
|
||||
async def function(data: None) -> None:
|
||||
...
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
class Class:
|
||||
def method():
|
||||
"""docstring"""
|
||||
# comment
|
||||
def function():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
try:
|
||||
if True:
|
||||
# comment
|
||||
class Class:
|
||||
pass
|
||||
|
||||
except:
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
def f():
|
||||
def f():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
class MyClass:
|
||||
# comment
|
||||
def method(self) -> None:
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
def function1():
|
||||
# Comment
|
||||
def function2():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
async def function1():
|
||||
await function2()
|
||||
async with function3():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
if (
|
||||
cond1
|
||||
|
||||
|
||||
|
||||
|
||||
and cond2
|
||||
):
|
||||
pass
|
||||
#end
|
||||
|
||||
|
||||
# no error
|
||||
async def function1():
|
||||
await function2()
|
||||
async with function3():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
async def function1():
|
||||
await function2()
|
||||
async with function3():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
async def function1():
|
||||
await function2()
|
||||
async with function3():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
class Test:
|
||||
async
|
||||
|
||||
def a(self): pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
class Test:
|
||||
def a():
|
||||
pass
|
||||
# wrongly indented comment
|
||||
|
||||
def b():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# no error
|
||||
def test():
|
||||
pass
|
||||
|
||||
# Wrongly indented comment
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E301
|
||||
class Class(object):
|
||||
|
||||
def func1():
|
||||
pass
|
||||
def func2():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E301
|
||||
class Class:
|
||||
|
||||
def fn1():
|
||||
pass
|
||||
# comment
|
||||
def fn2():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E302
|
||||
"""Main module."""
|
||||
def fn():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E302
|
||||
import sys
|
||||
def get_sys_path():
|
||||
return sys.path
|
||||
# end
|
||||
|
||||
|
||||
# E302
|
||||
def a():
|
||||
pass
|
||||
|
||||
def b():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E302
|
||||
def a():
|
||||
pass
|
||||
|
||||
# comment
|
||||
|
||||
def b():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E302
|
||||
def a():
|
||||
pass
|
||||
|
||||
async def b():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E302
|
||||
async def x():
|
||||
pass
|
||||
|
||||
async def x(y: int = 1):
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E302
|
||||
def bar():
|
||||
pass
|
||||
def baz(): pass
|
||||
# end
|
||||
|
||||
|
||||
# E302
|
||||
def bar(): pass
|
||||
def baz():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E302
|
||||
def f():
|
||||
pass
|
||||
|
||||
# comment
|
||||
@decorator
|
||||
def g():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E302
|
||||
class Test:
|
||||
|
||||
pass
|
||||
|
||||
def method1():
|
||||
return 1
|
||||
|
||||
|
||||
def method2():
|
||||
return 22
|
||||
# end
|
||||
|
||||
|
||||
# E303
|
||||
def fn():
|
||||
_ = None
|
||||
|
||||
|
||||
# arbitrary comment
|
||||
|
||||
def inner(): # E306 not expected (pycodestyle detects E306)
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E303
|
||||
def fn():
|
||||
_ = None
|
||||
|
||||
|
||||
# arbitrary comment
|
||||
def inner(): # E306 not expected (pycodestyle detects E306)
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E303
|
||||
print()
|
||||
|
||||
|
||||
|
||||
print()
|
||||
# end
|
||||
|
||||
|
||||
# E303:5:1
|
||||
print()
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
print()
|
||||
# end
|
||||
|
||||
|
||||
# E303:5:5 E303:8:5
|
||||
def a():
|
||||
print()
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
# another comment
|
||||
|
||||
print()
|
||||
# end
|
||||
|
||||
|
||||
# E303
|
||||
#!python
|
||||
|
||||
|
||||
|
||||
"""This class docstring comes on line 5.
|
||||
It gives error E303: too many blank lines (3)
|
||||
"""
|
||||
# end
|
||||
|
||||
|
||||
# E303
|
||||
class Class:
|
||||
def a(self):
|
||||
pass
|
||||
|
||||
|
||||
def b(self):
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E303
|
||||
if True:
|
||||
a = 1
|
||||
|
||||
|
||||
a = 2
|
||||
# end
|
||||
|
||||
|
||||
# E303
|
||||
class Test:
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
# another comment
|
||||
|
||||
def test(self): pass
|
||||
# end
|
||||
|
||||
|
||||
# E303
|
||||
class Test:
|
||||
def a(self):
|
||||
pass
|
||||
|
||||
# wrongly indented comment
|
||||
|
||||
|
||||
def b(self):
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E303
|
||||
def fn():
|
||||
pass
|
||||
|
||||
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E304
|
||||
@decorator
|
||||
|
||||
def function():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E304
|
||||
@decorator
|
||||
|
||||
# comment E304 not expected
|
||||
def function():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E304
|
||||
@decorator
|
||||
|
||||
# comment E304 not expected
|
||||
|
||||
|
||||
# second comment E304 not expected
|
||||
def function():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E305:7:1
|
||||
def fn():
|
||||
print()
|
||||
|
||||
# comment
|
||||
|
||||
# another comment
|
||||
fn()
|
||||
# end
|
||||
|
||||
|
||||
# E305
|
||||
class Class():
|
||||
pass
|
||||
|
||||
# comment
|
||||
|
||||
# another comment
|
||||
a = 1
|
||||
# end
|
||||
|
||||
|
||||
# E305:8:1
|
||||
def fn():
|
||||
print()
|
||||
|
||||
# comment
|
||||
|
||||
# another comment
|
||||
|
||||
try:
|
||||
fn()
|
||||
except Exception:
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E305:5:1
|
||||
def a():
|
||||
print()
|
||||
|
||||
# Two spaces before comments, too.
|
||||
if a():
|
||||
a()
|
||||
# end
|
||||
|
||||
|
||||
#: E305:8:1
|
||||
# Example from https://github.com/PyCQA/pycodestyle/issues/400
|
||||
import stuff
|
||||
|
||||
|
||||
def main():
|
||||
blah, blah
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
# end
|
||||
|
||||
|
||||
# E306:3:5
|
||||
def a():
|
||||
x = 1
|
||||
def b():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
#: E306:3:5
|
||||
async def a():
|
||||
x = 1
|
||||
def b():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
#: E306:3:5 E306:5:9
|
||||
def a():
|
||||
x = 2
|
||||
def b():
|
||||
x = 1
|
||||
def c():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E306:3:5 E306:6:5
|
||||
def a():
|
||||
x = 1
|
||||
class C:
|
||||
pass
|
||||
x = 2
|
||||
def b():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E306
|
||||
def foo():
|
||||
def bar():
|
||||
pass
|
||||
def baz(): pass
|
||||
# end
|
||||
|
||||
|
||||
# E306:3:5
|
||||
def foo():
|
||||
def bar(): pass
|
||||
def baz():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E306
|
||||
def a():
|
||||
x = 2
|
||||
@decorator
|
||||
def b():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E306
|
||||
def a():
|
||||
x = 2
|
||||
@decorator
|
||||
async def b():
|
||||
pass
|
||||
# end
|
||||
|
||||
|
||||
# E306
|
||||
def a():
|
||||
x = 2
|
||||
async def b():
|
||||
pass
|
||||
# end
|
||||
@@ -1,2 +1,15 @@
|
||||
'''trailing whitespace
|
||||
inside a multiline string'''
|
||||
|
||||
f'''trailing whitespace
|
||||
inside a multiline f-string'''
|
||||
|
||||
# Trailing whitespace after `{`
|
||||
f'abc {
|
||||
1 + 2
|
||||
}'
|
||||
|
||||
# Trailing whitespace after `2`
|
||||
f'abc {
|
||||
1 + 2
|
||||
}'
|
||||
|
||||
@@ -562,3 +562,46 @@ def titlecase_sub_section_header():
|
||||
|
||||
Returns:
|
||||
"""
|
||||
|
||||
|
||||
def test_method_should_be_correctly_capitalized(parameters: list[str], other_parameters: dict[str, str]): # noqa: D213
|
||||
"""Test parameters and attributes sections are capitalized correctly.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parameters:
|
||||
A list of string parameters
|
||||
other_parameters:
|
||||
A dictionary of string attributes
|
||||
|
||||
Other Parameters
|
||||
----------
|
||||
other_parameters:
|
||||
A dictionary of string attributes
|
||||
parameters:
|
||||
A list of string parameters
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def test_lowercase_sub_section_header_should_be_valid(parameters: list[str], value: int): # noqa: D213
|
||||
"""Test that lower case subsection header is valid even if it has the same name as section kind.
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
parameters:
|
||||
A list of string parameters
|
||||
value:
|
||||
Some value
|
||||
"""
|
||||
|
||||
|
||||
def test_lowercase_sub_section_header_different_kind(returns: int):
|
||||
"""Test that lower case subsection header is valid even if it is of a different kind.
|
||||
|
||||
Parameters
|
||||
-‐-----------------
|
||||
returns:
|
||||
some value
|
||||
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
1 in (
|
||||
1, 2, 3
|
||||
)
|
||||
|
||||
# OK
|
||||
fruits = ["cherry", "grapes"]
|
||||
"cherry" in fruits
|
||||
_ = {key: value for key, value in {"a": 1, "b": 2}.items() if key in ("a", "b")}
|
||||
|
||||
# OK
|
||||
fruits in [[1, 2, 3], [4, 5, 6]]
|
||||
fruits in [1, 2, 3]
|
||||
1 in [[1, 2, 3], [4, 5, 6]]
|
||||
_ = {key: value for key, value in {"a": 1, "b": 2}.items() if key in (["a", "b"], ["c", "d"])}
|
||||
|
||||
@@ -35,6 +35,15 @@ if argc != 0: # correct
|
||||
if argc != 1: # correct
|
||||
pass
|
||||
|
||||
if argc != -1.0: # correct
|
||||
pass
|
||||
|
||||
if argc != 0.0: # correct
|
||||
pass
|
||||
|
||||
if argc != 1.0: # correct
|
||||
pass
|
||||
|
||||
if argc != 2: # [magic-value-comparison]
|
||||
pass
|
||||
|
||||
@@ -44,6 +53,12 @@ if argc != -2: # [magic-value-comparison]
|
||||
if argc != +2: # [magic-value-comparison]
|
||||
pass
|
||||
|
||||
if argc != -2.0: # [magic-value-comparison]
|
||||
pass
|
||||
|
||||
if argc != +2.0: # [magic-value-comparison]
|
||||
pass
|
||||
|
||||
if __name__ == "__main__": # correct
|
||||
pass
|
||||
|
||||
|
||||
@@ -46,3 +46,8 @@ x: typing.TypeAlias = list[T]
|
||||
# OK
|
||||
x: TypeAlias
|
||||
x: int = 1
|
||||
|
||||
# Ensure that "T" appears only once in the type parameters for the modernized
|
||||
# type alias.
|
||||
T = typing.TypeVar["T"]
|
||||
Decorator: TypeAlias = typing.Callable[[T], T]
|
||||
|
||||
75
crates/ruff_linter/resources/test/fixtures/refurb/FURB129.py
vendored
Normal file
75
crates/ruff_linter/resources/test/fixtures/refurb/FURB129.py
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
import codecs
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
# Errors
|
||||
with open("FURB129.py") as f:
|
||||
for _line in f.readlines():
|
||||
pass
|
||||
a = [line.lower() for line in f.readlines()]
|
||||
b = {line.upper() for line in f.readlines()}
|
||||
c = {line.lower(): line.upper() for line in f.readlines()}
|
||||
|
||||
with Path("FURB129.py").open() as f:
|
||||
for _line in f.readlines():
|
||||
pass
|
||||
|
||||
for _line in open("FURB129.py").readlines():
|
||||
pass
|
||||
|
||||
for _line in Path("FURB129.py").open().readlines():
|
||||
pass
|
||||
|
||||
|
||||
def func():
|
||||
f = Path("FURB129.py").open()
|
||||
for _line in f.readlines():
|
||||
pass
|
||||
f.close()
|
||||
|
||||
|
||||
def func(f: io.BytesIO):
|
||||
for _line in f.readlines():
|
||||
pass
|
||||
|
||||
|
||||
def func():
|
||||
with (open("FURB129.py") as f, foo as bar):
|
||||
for _line in f.readlines():
|
||||
pass
|
||||
for _line in bar.readlines():
|
||||
pass
|
||||
|
||||
|
||||
# False positives
|
||||
def func(f):
|
||||
for _line in f.readlines():
|
||||
pass
|
||||
|
||||
|
||||
def func(f: codecs.StreamReader):
|
||||
for _line in f.readlines():
|
||||
pass
|
||||
|
||||
|
||||
def func():
|
||||
class A:
|
||||
def readlines(self) -> list[str]:
|
||||
return ["a", "b", "c"]
|
||||
|
||||
return A()
|
||||
|
||||
|
||||
for _line in func().readlines():
|
||||
pass
|
||||
|
||||
# OK
|
||||
for _line in ["a", "b", "c"]:
|
||||
pass
|
||||
with open("FURB129.py") as f:
|
||||
for _line in f:
|
||||
pass
|
||||
for _line in f.readlines(10):
|
||||
pass
|
||||
for _not_line in f.readline():
|
||||
pass
|
||||
@@ -162,3 +162,26 @@ async def f(x: bool):
|
||||
T = asyncio.create_task(asyncio.sleep(1))
|
||||
else:
|
||||
T = None
|
||||
|
||||
|
||||
# Error
|
||||
def f():
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.create_task(main()) # Error
|
||||
|
||||
# Error
|
||||
def f():
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(main()) # Error
|
||||
|
||||
# OK
|
||||
def f():
|
||||
global task
|
||||
loop = asyncio.new_event_loop()
|
||||
task = loop.create_task(main()) # Error
|
||||
|
||||
# OK
|
||||
def f():
|
||||
global task
|
||||
loop = asyncio.get_event_loop()
|
||||
task = loop.create_task(main()) # Error
|
||||
|
||||
@@ -250,6 +250,23 @@ __all__ = (
|
||||
,
|
||||
)
|
||||
|
||||
__all__ = ( # comment about the opening paren
|
||||
# multiline strange comment 0a
|
||||
# multiline strange comment 0b
|
||||
"foo" # inline comment about foo
|
||||
# multiline strange comment 1a
|
||||
# multiline strange comment 1b
|
||||
, # comment about the comma??
|
||||
# comment about bar part a
|
||||
# comment about bar part b
|
||||
"bar" # inline comment about bar
|
||||
# strange multiline comment comment 2a
|
||||
# strange multiline comment 2b
|
||||
,
|
||||
# strange multiline comment 3a
|
||||
# strange multiline comment 3b
|
||||
) # comment about the closing paren
|
||||
|
||||
###################################
|
||||
# These should all not get flagged:
|
||||
###################################
|
||||
|
||||
@@ -188,6 +188,10 @@ class BezierBuilder4:
|
||||
,
|
||||
)
|
||||
|
||||
__slots__ = {"foo", "bar",
|
||||
"baz", "bingo"
|
||||
}
|
||||
|
||||
###################################
|
||||
# These should all not get flagged:
|
||||
###################################
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
val = 2
|
||||
|
||||
def simple_cases():
|
||||
a = 4
|
||||
b = "{a}" # RUF027
|
||||
c = "{a} {b} f'{val}' " # RUF027
|
||||
|
||||
def escaped_string():
|
||||
a = 4
|
||||
b = "escaped string: {{ brackets surround me }}" # RUF027
|
||||
|
||||
def raw_string():
|
||||
a = 4
|
||||
b = r"raw string with formatting: {a}" # RUF027
|
||||
c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
|
||||
|
||||
def print_name(name: str):
|
||||
a = 4
|
||||
print("Hello, {name}!") # RUF027
|
||||
print("The test value we're using today is {a}") # RUF027
|
||||
|
||||
def do_nothing(a):
|
||||
return a
|
||||
|
||||
def nested_funcs():
|
||||
a = 4
|
||||
print(do_nothing(do_nothing("{a}"))) # RUF027
|
||||
|
||||
def tripled_quoted():
|
||||
a = 4
|
||||
c = a
|
||||
single_line = """ {a} """ # RUF027
|
||||
# RUF027
|
||||
multi_line = a = """b { # comment
|
||||
c} d
|
||||
"""
|
||||
|
||||
def single_quoted_multi_line():
|
||||
a = 4
|
||||
# RUF027
|
||||
b = " {\
|
||||
a} \
|
||||
"
|
||||
|
||||
def implicit_concat():
|
||||
a = 4
|
||||
b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
|
||||
print(f"{a}" "{a}" f"{b}") # RUF027
|
||||
|
||||
def escaped_chars():
|
||||
a = 4
|
||||
b = "\"not escaped:\" \'{a}\' \"escaped:\": \'{{c}}\'" # RUF027
|
||||
|
||||
def alternative_formatter(src, **kwargs):
|
||||
src.format(**kwargs)
|
||||
|
||||
def format2(src, *args):
|
||||
pass
|
||||
|
||||
# These should not cause an RUF027 message
|
||||
def negative_cases():
|
||||
a = 4
|
||||
positive = False
|
||||
"""{a}"""
|
||||
"don't format: {a}"
|
||||
c = """ {b} """
|
||||
d = "bad variable: {invalid}"
|
||||
e = "incorrect syntax: {}"
|
||||
json = "{ positive: false }"
|
||||
json2 = "{ 'positive': false }"
|
||||
json3 = "{ 'positive': 'false' }"
|
||||
alternative_formatter("{a}", a = 5)
|
||||
formatted = "{a}".fmt(a = 7)
|
||||
print(do_nothing("{a}".format(a=3)))
|
||||
print(do_nothing(alternative_formatter("{a}", a = 5)))
|
||||
print(format(do_nothing("{a}"), a = 5))
|
||||
print("{a}".to_upper())
|
||||
print(do_nothing("{a}").format(a = "Test"))
|
||||
print(do_nothing("{a}").format2(a))
|
||||
|
||||
a = 4
|
||||
|
||||
"always ignore this: {a}"
|
||||
|
||||
print("but don't ignore this: {val}") # RUF027
|
||||
70
crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py
vendored
Normal file
70
crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
val = 2
|
||||
|
||||
"always ignore this: {val}"
|
||||
|
||||
print("but don't ignore this: {val}") # RUF027
|
||||
|
||||
|
||||
def simple_cases():
|
||||
a = 4
|
||||
b = "{a}" # RUF027
|
||||
c = "{a} {b} f'{val}' " # RUF027
|
||||
|
||||
|
||||
def escaped_string():
|
||||
a = 4
|
||||
b = "escaped string: {{ brackets surround me }}" # RUF027
|
||||
|
||||
|
||||
def raw_string():
|
||||
a = 4
|
||||
b = r"raw string with formatting: {a}" # RUF027
|
||||
c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
|
||||
|
||||
|
||||
def print_name(name: str):
|
||||
a = 4
|
||||
print("Hello, {name}!") # RUF027
|
||||
print("The test value we're using today is {a}") # RUF027
|
||||
|
||||
|
||||
def nested_funcs():
|
||||
a = 4
|
||||
print(do_nothing(do_nothing("{a}"))) # RUF027
|
||||
|
||||
|
||||
def tripled_quoted():
|
||||
a = 4
|
||||
c = a
|
||||
single_line = """ {a} """ # RUF027
|
||||
# RUF027
|
||||
multi_line = a = """b { # comment
|
||||
c} d
|
||||
"""
|
||||
|
||||
|
||||
def single_quoted_multi_line():
|
||||
a = 4
|
||||
# RUF027
|
||||
b = " {\
|
||||
a} \
|
||||
"
|
||||
|
||||
|
||||
def implicit_concat():
|
||||
a = 4
|
||||
b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
|
||||
print(f"{a}" "{a}" f"{b}") # RUF027
|
||||
|
||||
|
||||
def escaped_chars():
|
||||
a = 4
|
||||
b = "\"not escaped:\" '{a}' \"escaped:\": '{{c}}'" # RUF027
|
||||
|
||||
|
||||
def method_calls():
|
||||
value = {}
|
||||
value.method = print_name
|
||||
first = "Wendy"
|
||||
last = "Appleseed"
|
||||
value.method("{first} {last}") # RUF027
|
||||
36
crates/ruff_linter/resources/test/fixtures/ruff/RUF027_1.py
vendored
Normal file
36
crates/ruff_linter/resources/test/fixtures/ruff/RUF027_1.py
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
def do_nothing(a):
|
||||
return a
|
||||
|
||||
|
||||
def alternative_formatter(src, **kwargs):
|
||||
src.format(**kwargs)
|
||||
|
||||
|
||||
def format2(src, *args):
|
||||
pass
|
||||
|
||||
|
||||
# These should not cause an RUF027 message
|
||||
def negative_cases():
|
||||
a = 4
|
||||
positive = False
|
||||
"""{a}"""
|
||||
"don't format: {a}"
|
||||
c = """ {b} """
|
||||
d = "bad variable: {invalid}"
|
||||
e = "incorrect syntax: {}"
|
||||
f = "uses a builtin: {max}"
|
||||
json = "{ positive: false }"
|
||||
json2 = "{ 'positive': false }"
|
||||
json3 = "{ 'positive': 'false' }"
|
||||
alternative_formatter("{a}", a=5)
|
||||
formatted = "{a}".fmt(a=7)
|
||||
print(do_nothing("{a}".format(a=3)))
|
||||
print(do_nothing(alternative_formatter("{a}", a=5)))
|
||||
print(format(do_nothing("{a}"), a=5))
|
||||
print("{a}".to_upper())
|
||||
print(do_nothing("{a}").format(a="Test"))
|
||||
print(do_nothing("{a}").format2(a))
|
||||
print(("{a}" "{c}").format(a=1, c=2))
|
||||
print("{a}".attribute.chaining.call(a=2))
|
||||
print("{a} {c}".format(a))
|
||||
@@ -2,11 +2,14 @@ use ruff_python_ast::Comprehension;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::flake8_simplify;
|
||||
use crate::rules::{flake8_simplify, refurb};
|
||||
|
||||
/// Run lint rules over a [`Comprehension`] syntax nodes.
|
||||
pub(crate) fn comprehension(comprehension: &Comprehension, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::InDictKeys) {
|
||||
flake8_simplify::rules::key_in_dict_comprehension(checker, comprehension);
|
||||
}
|
||||
if checker.enabled(Rule::ReadlinesInFor) {
|
||||
refurb::rules::readlines_in_comprehension(checker, comprehension);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,17 +281,21 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::UnusedPrivateTypeVar) {
|
||||
flake8_pyi::rules::unused_private_type_var(checker, scope, &mut diagnostics);
|
||||
}
|
||||
if checker.enabled(Rule::UnusedPrivateProtocol) {
|
||||
flake8_pyi::rules::unused_private_protocol(checker, scope, &mut diagnostics);
|
||||
}
|
||||
if checker.enabled(Rule::UnusedPrivateTypeAlias) {
|
||||
flake8_pyi::rules::unused_private_type_alias(checker, scope, &mut diagnostics);
|
||||
}
|
||||
if checker.enabled(Rule::UnusedPrivateTypedDict) {
|
||||
flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics);
|
||||
if checker.source_type.is_stub()
|
||||
|| matches!(scope.kind, ScopeKind::Module | ScopeKind::Function(_))
|
||||
{
|
||||
if checker.enabled(Rule::UnusedPrivateTypeVar) {
|
||||
flake8_pyi::rules::unused_private_type_var(checker, scope, &mut diagnostics);
|
||||
}
|
||||
if checker.enabled(Rule::UnusedPrivateProtocol) {
|
||||
flake8_pyi::rules::unused_private_protocol(checker, scope, &mut diagnostics);
|
||||
}
|
||||
if checker.enabled(Rule::UnusedPrivateTypeAlias) {
|
||||
flake8_pyi::rules::unused_private_type_alias(checker, scope, &mut diagnostics);
|
||||
}
|
||||
if checker.enabled(Rule::UnusedPrivateTypedDict) {
|
||||
flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::AsyncioDanglingTask) {
|
||||
|
||||
@@ -247,6 +247,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::HardcodedPasswordDefault) {
|
||||
flake8_bandit::rules::hardcoded_password_default(checker, parameters);
|
||||
}
|
||||
if checker.enabled(Rule::SuspiciousMarkSafeUsage) {
|
||||
for decorator in decorator_list {
|
||||
flake8_bandit::rules::suspicious_function_decorator(checker, decorator);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::PropertyWithParameters) {
|
||||
pylint::rules::property_with_parameters(checker, stmt, decorator_list, parameters);
|
||||
}
|
||||
@@ -1312,6 +1317,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
|
||||
pylint::rules::unnecessary_dict_index_lookup(checker, for_stmt);
|
||||
}
|
||||
if checker.enabled(Rule::ReadlinesInFor) {
|
||||
refurb::rules::readlines_in_for(checker, for_stmt);
|
||||
}
|
||||
if !is_async {
|
||||
if checker.enabled(Rule::ReimplementedBuiltin) {
|
||||
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
|
||||
|
||||
@@ -31,8 +31,8 @@ use std::path::Path;
|
||||
use itertools::Itertools;
|
||||
use log::debug;
|
||||
use ruff_python_ast::{
|
||||
self as ast, Arguments, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext,
|
||||
Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt, Suite, UnaryOp,
|
||||
self as ast, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext, Keyword,
|
||||
MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt, Suite, UnaryOp,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
@@ -40,7 +40,7 @@ use ruff_diagnostics::{Diagnostic, IsolationLevel};
|
||||
use ruff_notebook::{CellOffsets, NotebookIndex};
|
||||
use ruff_python_ast::all::{extract_all_names, DunderAllFlags};
|
||||
use ruff_python_ast::helpers::{
|
||||
collect_import_from_member, extract_handled_exceptions, to_module_path,
|
||||
collect_import_from_member, extract_handled_exceptions, is_docstring_stmt, to_module_path,
|
||||
};
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::str::trailing_quote;
|
||||
@@ -71,6 +71,38 @@ mod analyze;
|
||||
mod annotation;
|
||||
mod deferred;
|
||||
|
||||
/// State representing whether a docstring is expected or not for the next statement.
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq)]
|
||||
enum DocstringState {
|
||||
/// The next statement is expected to be a docstring, but not necessarily so.
|
||||
///
|
||||
/// For example, in the following code:
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// pass
|
||||
///
|
||||
///
|
||||
/// def bar(x, y):
|
||||
/// """Docstring."""
|
||||
/// return x + y
|
||||
/// ```
|
||||
///
|
||||
/// For `Foo`, the state is expected when the checker is visiting the class
|
||||
/// body but isn't going to be present. While, for `bar` function, the docstring
|
||||
/// is expected and present.
|
||||
#[default]
|
||||
Expected,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl DocstringState {
|
||||
/// Returns `true` if the next statement is expected to be a docstring.
|
||||
const fn is_expected(self) -> bool {
|
||||
matches!(self, DocstringState::Expected)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Checker<'a> {
|
||||
/// The [`Path`] to the file under analysis.
|
||||
path: &'a Path,
|
||||
@@ -114,6 +146,8 @@ pub(crate) struct Checker<'a> {
|
||||
pub(crate) flake8_bugbear_seen: Vec<TextRange>,
|
||||
/// The end offset of the last visited statement.
|
||||
last_stmt_end: TextSize,
|
||||
/// A state describing if a docstring is expected or not.
|
||||
docstring_state: DocstringState,
|
||||
}
|
||||
|
||||
impl<'a> Checker<'a> {
|
||||
@@ -153,6 +187,7 @@ impl<'a> Checker<'a> {
|
||||
cell_offsets,
|
||||
notebook_index,
|
||||
last_stmt_end: TextSize::default(),
|
||||
docstring_state: DocstringState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,19 +340,16 @@ where
|
||||
self.semantic.flags -= SemanticModelFlags::IMPORT_BOUNDARY;
|
||||
}
|
||||
|
||||
// Track whether we've seen docstrings, non-imports, etc.
|
||||
// Track whether we've seen module docstrings, non-imports, etc.
|
||||
match stmt {
|
||||
Stmt::Expr(ast::StmtExpr { value, .. })
|
||||
if !self
|
||||
.semantic
|
||||
.flags
|
||||
.intersects(SemanticModelFlags::MODULE_DOCSTRING)
|
||||
if !self.semantic.seen_module_docstring_boundary()
|
||||
&& value.is_string_literal_expr() =>
|
||||
{
|
||||
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING;
|
||||
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY;
|
||||
}
|
||||
Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) => {
|
||||
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING;
|
||||
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY;
|
||||
|
||||
// Allow __future__ imports until we see a non-__future__ import.
|
||||
if let Some("__future__") = module.as_deref() {
|
||||
@@ -332,11 +364,11 @@ where
|
||||
}
|
||||
}
|
||||
Stmt::Import(_) => {
|
||||
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING;
|
||||
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY;
|
||||
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
|
||||
}
|
||||
_ => {
|
||||
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING;
|
||||
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY;
|
||||
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
|
||||
if !(self.semantic.seen_import_boundary()
|
||||
|| helpers::is_assignment_to_a_dunder(stmt)
|
||||
@@ -353,6 +385,16 @@ where
|
||||
// the node.
|
||||
let flags_snapshot = self.semantic.flags;
|
||||
|
||||
// Update the semantic model if it is in a docstring. This should be done after the
|
||||
// flags snapshot to ensure that it gets reset once the statement is analyzed.
|
||||
if self.docstring_state.is_expected() {
|
||||
if is_docstring_stmt(stmt) {
|
||||
self.semantic.flags |= SemanticModelFlags::DOCSTRING;
|
||||
}
|
||||
// Reset the state irrespective of whether the statement is a docstring or not.
|
||||
self.docstring_state = DocstringState::Other;
|
||||
}
|
||||
|
||||
// Step 1: Binding
|
||||
match stmt {
|
||||
Stmt::AugAssign(ast::StmtAugAssign {
|
||||
@@ -654,6 +696,8 @@ where
|
||||
self.semantic.set_globals(globals);
|
||||
}
|
||||
|
||||
// Set the docstring state before visiting the class body.
|
||||
self.docstring_state = DocstringState::Expected;
|
||||
self.visit_body(body);
|
||||
}
|
||||
Stmt::TypeAlias(ast::StmtTypeAlias {
|
||||
@@ -989,12 +1033,7 @@ where
|
||||
}
|
||||
Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
arguments:
|
||||
Arguments {
|
||||
args,
|
||||
keywords,
|
||||
range: _,
|
||||
},
|
||||
arguments,
|
||||
range: _,
|
||||
}) => {
|
||||
self.visit_expr(func);
|
||||
@@ -1037,7 +1076,7 @@ where
|
||||
});
|
||||
match callable {
|
||||
Some(typing::Callable::Bool) => {
|
||||
let mut args = args.iter();
|
||||
let mut args = arguments.args.iter();
|
||||
if let Some(arg) = args.next() {
|
||||
self.visit_boolean_test(arg);
|
||||
}
|
||||
@@ -1046,7 +1085,7 @@ where
|
||||
}
|
||||
}
|
||||
Some(typing::Callable::Cast) => {
|
||||
let mut args = args.iter();
|
||||
let mut args = arguments.args.iter();
|
||||
if let Some(arg) = args.next() {
|
||||
self.visit_type_definition(arg);
|
||||
}
|
||||
@@ -1055,7 +1094,7 @@ where
|
||||
}
|
||||
}
|
||||
Some(typing::Callable::NewType) => {
|
||||
let mut args = args.iter();
|
||||
let mut args = arguments.args.iter();
|
||||
if let Some(arg) = args.next() {
|
||||
self.visit_non_type_definition(arg);
|
||||
}
|
||||
@@ -1064,21 +1103,21 @@ where
|
||||
}
|
||||
}
|
||||
Some(typing::Callable::TypeVar) => {
|
||||
let mut args = args.iter();
|
||||
let mut args = arguments.args.iter();
|
||||
if let Some(arg) = args.next() {
|
||||
self.visit_non_type_definition(arg);
|
||||
}
|
||||
for arg in args {
|
||||
self.visit_type_definition(arg);
|
||||
}
|
||||
for keyword in keywords {
|
||||
for keyword in arguments.keywords.iter() {
|
||||
let Keyword {
|
||||
arg,
|
||||
value,
|
||||
range: _,
|
||||
} = keyword;
|
||||
if let Some(id) = arg {
|
||||
if id == "bound" {
|
||||
if id.as_str() == "bound" {
|
||||
self.visit_type_definition(value);
|
||||
} else {
|
||||
self.visit_non_type_definition(value);
|
||||
@@ -1088,7 +1127,7 @@ where
|
||||
}
|
||||
Some(typing::Callable::NamedTuple) => {
|
||||
// Ex) NamedTuple("a", [("a", int)])
|
||||
let mut args = args.iter();
|
||||
let mut args = arguments.args.iter();
|
||||
if let Some(arg) = args.next() {
|
||||
self.visit_non_type_definition(arg);
|
||||
}
|
||||
@@ -1117,7 +1156,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
for keyword in keywords {
|
||||
for keyword in arguments.keywords.iter() {
|
||||
let Keyword { arg, value, .. } = keyword;
|
||||
match (arg.as_ref(), value) {
|
||||
// Ex) NamedTuple("a", **{"a": int})
|
||||
@@ -1144,7 +1183,7 @@ where
|
||||
}
|
||||
Some(typing::Callable::TypedDict) => {
|
||||
// Ex) TypedDict("a", {"a": int})
|
||||
let mut args = args.iter();
|
||||
let mut args = arguments.args.iter();
|
||||
if let Some(arg) = args.next() {
|
||||
self.visit_non_type_definition(arg);
|
||||
}
|
||||
@@ -1167,13 +1206,13 @@ where
|
||||
}
|
||||
|
||||
// Ex) TypedDict("a", a=int)
|
||||
for keyword in keywords {
|
||||
for keyword in arguments.keywords.iter() {
|
||||
let Keyword { value, .. } = keyword;
|
||||
self.visit_type_definition(value);
|
||||
}
|
||||
}
|
||||
Some(typing::Callable::MypyExtension) => {
|
||||
let mut args = args.iter();
|
||||
let mut args = arguments.args.iter();
|
||||
if let Some(arg) = args.next() {
|
||||
// Ex) DefaultNamedArg(bool | None, name="some_prop_name")
|
||||
self.visit_type_definition(arg);
|
||||
@@ -1181,13 +1220,13 @@ where
|
||||
for arg in args {
|
||||
self.visit_non_type_definition(arg);
|
||||
}
|
||||
for keyword in keywords {
|
||||
for keyword in arguments.keywords.iter() {
|
||||
let Keyword { value, .. } = keyword;
|
||||
self.visit_non_type_definition(value);
|
||||
}
|
||||
} else {
|
||||
// Ex) DefaultNamedArg(type="bool", name="some_prop_name")
|
||||
for keyword in keywords {
|
||||
for keyword in arguments.keywords.iter() {
|
||||
let Keyword {
|
||||
value,
|
||||
arg,
|
||||
@@ -1205,10 +1244,10 @@ where
|
||||
// If we're in a type definition, we need to treat the arguments to any
|
||||
// other callables as non-type definitions (i.e., we don't want to treat
|
||||
// any strings as deferred type definitions).
|
||||
for arg in args {
|
||||
for arg in arguments.args.iter() {
|
||||
self.visit_non_type_definition(arg);
|
||||
}
|
||||
for keyword in keywords {
|
||||
for keyword in arguments.keywords.iter() {
|
||||
let Keyword { value, .. } = keyword;
|
||||
self.visit_non_type_definition(value);
|
||||
}
|
||||
@@ -1293,6 +1332,16 @@ where
|
||||
self.semantic.flags |= SemanticModelFlags::F_STRING;
|
||||
visitor::walk_expr(self, expr);
|
||||
}
|
||||
Expr::NamedExpr(ast::ExprNamedExpr {
|
||||
target,
|
||||
value,
|
||||
range: _,
|
||||
}) => {
|
||||
self.visit_expr(value);
|
||||
|
||||
self.semantic.flags |= SemanticModelFlags::NAMED_EXPRESSION_ASSIGNMENT;
|
||||
self.visit_expr(target);
|
||||
}
|
||||
_ => visitor::walk_expr(self, expr),
|
||||
}
|
||||
|
||||
@@ -1509,6 +1558,8 @@ impl<'a> Checker<'a> {
|
||||
unreachable!("Generator expression must contain at least one generator");
|
||||
};
|
||||
|
||||
let flags = self.semantic.flags;
|
||||
|
||||
// Generators are compiled as nested functions. (This may change with PEP 709.)
|
||||
// As such, the `iter` of the first generator is evaluated in the outer scope, while all
|
||||
// subsequent nodes are evaluated in the inner scope.
|
||||
@@ -1538,14 +1589,22 @@ impl<'a> Checker<'a> {
|
||||
// `x` is local to `foo`, and the `T` in `y=T` skips the class scope when resolving.
|
||||
self.visit_expr(&generator.iter);
|
||||
self.semantic.push_scope(ScopeKind::Generator);
|
||||
|
||||
self.semantic.flags = flags | SemanticModelFlags::COMPREHENSION_ASSIGNMENT;
|
||||
self.visit_expr(&generator.target);
|
||||
self.semantic.flags = flags;
|
||||
|
||||
for expr in &generator.ifs {
|
||||
self.visit_boolean_test(expr);
|
||||
}
|
||||
|
||||
for generator in iterator {
|
||||
self.visit_expr(&generator.iter);
|
||||
|
||||
self.semantic.flags = flags | SemanticModelFlags::COMPREHENSION_ASSIGNMENT;
|
||||
self.visit_expr(&generator.target);
|
||||
self.semantic.flags = flags;
|
||||
|
||||
for expr in &generator.ifs {
|
||||
self.visit_boolean_test(expr);
|
||||
}
|
||||
@@ -1744,11 +1803,21 @@ impl<'a> Checker<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
// A binding within a `for` must be a loop variable, as in:
|
||||
// ```python
|
||||
// for x in range(10):
|
||||
// ...
|
||||
// ```
|
||||
if parent.is_for_stmt() {
|
||||
self.add_binding(id, expr.range(), BindingKind::LoopVar, flags);
|
||||
return;
|
||||
}
|
||||
|
||||
// A binding within a `with` must be an item, as in:
|
||||
// ```python
|
||||
// with open("file.txt") as fp:
|
||||
// ...
|
||||
// ```
|
||||
if parent.is_with_stmt() {
|
||||
self.add_binding(id, expr.range(), BindingKind::WithItemVar, flags);
|
||||
return;
|
||||
@@ -1804,17 +1873,26 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
|
||||
// If the expression is the left-hand side of a walrus operator, then it's a named
|
||||
// expression assignment.
|
||||
if self
|
||||
.semantic
|
||||
.current_expressions()
|
||||
.filter_map(Expr::as_named_expr_expr)
|
||||
.any(|parent| parent.target.as_ref() == expr)
|
||||
{
|
||||
// expression assignment, as in:
|
||||
// ```python
|
||||
// if (x := 10) > 5:
|
||||
// ...
|
||||
// ```
|
||||
if self.semantic.in_named_expression_assignment() {
|
||||
self.add_binding(id, expr.range(), BindingKind::NamedExprAssignment, flags);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the expression is part of a comprehension target, then it's a comprehension variable
|
||||
// assignment, as in:
|
||||
// ```python
|
||||
// [x for x in range(10)]
|
||||
// ```
|
||||
if self.semantic.in_comprehension_assignment() {
|
||||
self.add_binding(id, expr.range(), BindingKind::ComprehensionVar, flags);
|
||||
return;
|
||||
}
|
||||
|
||||
self.add_binding(id, expr.range(), BindingKind::Assignment, flags);
|
||||
}
|
||||
|
||||
@@ -1930,6 +2008,8 @@ impl<'a> Checker<'a> {
|
||||
};
|
||||
|
||||
self.visit_parameters(parameters);
|
||||
// Set the docstring state before visiting the function body.
|
||||
self.docstring_state = DocstringState::Expected;
|
||||
self.visit_body(body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::line_width::IndentWidth;
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
@@ -15,11 +16,11 @@ use crate::rules::pycodestyle::rules::logical_lines::{
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
/// Return the amount of indentation, expanding tabs to the next multiple of the settings' tab size.
|
||||
fn expand_indent(line: &str, settings: &LinterSettings) -> usize {
|
||||
pub(crate) fn expand_indent(line: &str, indent_width: IndentWidth) -> usize {
|
||||
let line = line.trim_end_matches(['\n', '\r']);
|
||||
|
||||
let mut indent = 0;
|
||||
let tab_size = settings.tab_size.as_usize();
|
||||
let tab_size = indent_width.as_usize();
|
||||
for c in line.bytes() {
|
||||
match c {
|
||||
b'\t' => indent = (indent / tab_size) * tab_size + tab_size,
|
||||
@@ -85,7 +86,7 @@ pub(crate) fn check_logical_lines(
|
||||
TextRange::new(locator.line_start(first_token.start()), first_token.start())
|
||||
};
|
||||
|
||||
let indent_level = expand_indent(locator.slice(range), settings);
|
||||
let indent_level = expand_indent(locator.slice(range), settings.tab_size);
|
||||
|
||||
let indent_size = 4;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::path::Path;
|
||||
|
||||
use ruff_notebook::CellOffsets;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::Tok;
|
||||
|
||||
@@ -14,6 +15,7 @@ use ruff_source_file::Locator;
|
||||
use crate::directives::TodoComment;
|
||||
use crate::lex::docstring_detection::StateMachine;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::pycodestyle::rules::BlankLinesChecker;
|
||||
use crate::rules::ruff::rules::Context;
|
||||
use crate::rules::{
|
||||
eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
|
||||
@@ -21,17 +23,37 @@ use crate::rules::{
|
||||
};
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn check_tokens(
|
||||
tokens: &[LexResult],
|
||||
path: &Path,
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
stylist: &Stylist,
|
||||
settings: &LinterSettings,
|
||||
source_type: PySourceType,
|
||||
cell_offsets: Option<&CellOffsets>,
|
||||
) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = vec![];
|
||||
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::BlankLineBetweenMethods,
|
||||
Rule::BlankLinesTopLevel,
|
||||
Rule::TooManyBlankLines,
|
||||
Rule::BlankLineAfterDecorator,
|
||||
Rule::BlankLinesAfterFunctionOrClass,
|
||||
Rule::BlankLinesBeforeNestedDefinition,
|
||||
]) {
|
||||
let mut blank_lines_checker = BlankLinesChecker::default();
|
||||
blank_lines_checker.check_lines(
|
||||
tokens,
|
||||
locator,
|
||||
stylist,
|
||||
settings.tab_size,
|
||||
&mut diagnostics,
|
||||
);
|
||||
}
|
||||
|
||||
if settings.rules.enabled(Rule::BlanketNOQA) {
|
||||
pygrep_hooks::rules::blanket_noqa(&mut diagnostics, indexer, locator);
|
||||
}
|
||||
@@ -95,7 +117,7 @@ pub(crate) fn check_tokens(
|
||||
}
|
||||
|
||||
if settings.rules.enabled(Rule::TabIndentation) {
|
||||
pycodestyle::rules::tab_indentation(&mut diagnostics, tokens, locator, indexer);
|
||||
pycodestyle::rules::tab_indentation(&mut diagnostics, locator, indexer);
|
||||
}
|
||||
|
||||
if settings.rules.any_enabled(&[
|
||||
|
||||
@@ -137,6 +137,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pycodestyle, "E274") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E275") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword),
|
||||
(Pycodestyle, "E301") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLineBetweenMethods),
|
||||
(Pycodestyle, "E302") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesTopLevel),
|
||||
(Pycodestyle, "E303") => (RuleGroup::Preview, rules::pycodestyle::rules::TooManyBlankLines),
|
||||
(Pycodestyle, "E304") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLineAfterDecorator),
|
||||
(Pycodestyle, "E305") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesAfterFunctionOrClass),
|
||||
(Pycodestyle, "E306") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesBeforeNestedDefinition),
|
||||
(Pycodestyle, "E401") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleImportsOnOneLine),
|
||||
(Pycodestyle, "E402") => (RuleGroup::Stable, rules::pycodestyle::rules::ModuleImportNotAtTopOfFile),
|
||||
(Pycodestyle, "E501") => (RuleGroup::Stable, rules::pycodestyle::rules::LineTooLong),
|
||||
@@ -1019,6 +1025,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
#[allow(deprecated)]
|
||||
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
|
||||
(Refurb, "118") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedOperator),
|
||||
(Refurb, "129") => (RuleGroup::Preview, rules::refurb::rules::ReadlinesInFor),
|
||||
#[allow(deprecated)]
|
||||
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
|
||||
#[allow(deprecated)]
|
||||
|
||||
@@ -5,7 +5,7 @@ use ruff_python_ast::docstrings::{leading_space, leading_words};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use ruff_source_file::{Line, UniversalNewlineIterator, UniversalNewlines};
|
||||
use ruff_source_file::{Line, NewlineWithTrailingNewline, UniversalNewlines};
|
||||
|
||||
use crate::docstrings::styles::SectionStyle;
|
||||
use crate::docstrings::{Docstring, DocstringBody};
|
||||
@@ -130,6 +130,34 @@ impl SectionKind {
|
||||
Self::Yields => "Yields",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if a section can contain subsections, as in:
|
||||
/// ```python
|
||||
/// Yields
|
||||
/// ------
|
||||
/// int
|
||||
/// Description of the anonymous integer return value.
|
||||
/// ```
|
||||
///
|
||||
/// For NumPy, see: <https://numpydoc.readthedocs.io/en/latest/format.html>
|
||||
///
|
||||
/// For Google, see: <https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings>
|
||||
pub(crate) fn has_subsections(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Args
|
||||
| Self::Arguments
|
||||
| Self::OtherArgs
|
||||
| Self::OtherParameters
|
||||
| Self::OtherParams
|
||||
| Self::Parameters
|
||||
| Self::Raises
|
||||
| Self::Returns
|
||||
| Self::SeeAlso
|
||||
| Self::Warns
|
||||
| Self::Yields
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SectionContexts<'a> {
|
||||
@@ -356,13 +384,16 @@ impl<'a> SectionContext<'a> {
|
||||
pub(crate) fn previous_line(&self) -> Option<&'a str> {
|
||||
let previous =
|
||||
&self.docstring_body.as_str()[TextRange::up_to(self.range_relative().start())];
|
||||
previous.universal_newlines().last().map(|l| l.as_str())
|
||||
previous
|
||||
.universal_newlines()
|
||||
.last()
|
||||
.map(|line| line.as_str())
|
||||
}
|
||||
|
||||
/// Returns the lines belonging to this section after the summary line.
|
||||
pub(crate) fn following_lines(&self) -> UniversalNewlineIterator<'a> {
|
||||
pub(crate) fn following_lines(&self) -> NewlineWithTrailingNewline<'a> {
|
||||
let lines = self.following_lines_str();
|
||||
UniversalNewlineIterator::with_offset(lines, self.offset() + self.data.summary_full_end)
|
||||
NewlineWithTrailingNewline::with_offset(lines, self.offset() + self.data.summary_full_end)
|
||||
}
|
||||
|
||||
fn following_lines_str(&self) -> &'a str {
|
||||
@@ -459,13 +490,54 @@ fn is_docstring_section(
|
||||
// args: The arguments to the function.
|
||||
// """
|
||||
// ```
|
||||
// Or `parameters` in:
|
||||
// ```python
|
||||
// def func(parameters: tuple[int]):
|
||||
// """Toggle the gizmo.
|
||||
//
|
||||
// Parameters:
|
||||
// -----
|
||||
// parameters:
|
||||
// The arguments to the function.
|
||||
// """
|
||||
// ```
|
||||
// However, if the header is an _exact_ match (like `Returns:`, as opposed to `returns:`), then
|
||||
// continue to treat it as a section header.
|
||||
if let Some(previous_section) = previous_section {
|
||||
if previous_section.indent_size < indent_size {
|
||||
if section_kind.has_subsections() {
|
||||
if let Some(previous_section) = previous_section {
|
||||
let verbatim = &line[TextRange::at(indent_size, section_name_size)];
|
||||
if section_kind.as_str() != verbatim {
|
||||
return false;
|
||||
|
||||
// If the section is more deeply indented, assume it's a subsection, as in:
|
||||
// ```python
|
||||
// def func(args: tuple[int]):
|
||||
// """Toggle the gizmo.
|
||||
//
|
||||
// Args:
|
||||
// args: The arguments to the function.
|
||||
// """
|
||||
// ```
|
||||
if previous_section.indent_size < indent_size {
|
||||
if section_kind.as_str() != verbatim {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the section isn't underlined, and isn't title-cased, assume it's a subsection,
|
||||
// as in:
|
||||
// ```python
|
||||
// def func(parameters: tuple[int]):
|
||||
// """Toggle the gizmo.
|
||||
//
|
||||
// Parameters:
|
||||
// -----
|
||||
// parameters:
|
||||
// The arguments to the function.
|
||||
// """
|
||||
// ```
|
||||
if !next_line_is_underline && verbatim.chars().next().is_some_and(char::is_lowercase) {
|
||||
if section_kind.as_str() != verbatim {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ pub fn check_path(
|
||||
path,
|
||||
locator,
|
||||
indexer,
|
||||
stylist,
|
||||
settings,
|
||||
source_type,
|
||||
source_kind.as_ipy_notebook().map(Notebook::cell_offsets),
|
||||
|
||||
@@ -264,6 +264,11 @@ impl Rule {
|
||||
| Rule::BadQuotesMultilineString
|
||||
| Rule::BlanketNOQA
|
||||
| Rule::BlanketTypeIgnore
|
||||
| Rule::BlankLineAfterDecorator
|
||||
| Rule::BlankLineBetweenMethods
|
||||
| Rule::BlankLinesAfterFunctionOrClass
|
||||
| Rule::BlankLinesBeforeNestedDefinition
|
||||
| Rule::BlankLinesTopLevel
|
||||
| Rule::CommentedOutCode
|
||||
| Rule::EmptyComment
|
||||
| Rule::ExtraneousParentheses
|
||||
@@ -296,6 +301,7 @@ impl Rule {
|
||||
| Rule::ShebangNotFirstLine
|
||||
| Rule::SingleLineImplicitStringConcatenation
|
||||
| Rule::TabIndentation
|
||||
| Rule::TooManyBlankLines
|
||||
| Rule::TrailingCommaOnBareTuple
|
||||
| Rule::TypeCommentInStub
|
||||
| Rule::UselessSemicolon
|
||||
|
||||
@@ -248,6 +248,7 @@ impl Renamer {
|
||||
| BindingKind::Assignment
|
||||
| BindingKind::BoundException
|
||||
| BindingKind::LoopVar
|
||||
| BindingKind::ComprehensionVar
|
||||
| BindingKind::WithItemVar
|
||||
| BindingKind::Global
|
||||
| BindingKind::Nonlocal(_)
|
||||
|
||||
@@ -321,6 +321,16 @@ mod schema {
|
||||
true
|
||||
}
|
||||
})
|
||||
.filter(|_rule| {
|
||||
// Filter out all test-only rules
|
||||
#[cfg(feature = "test-rules")]
|
||||
#[allow(clippy::used_underscore_binding)]
|
||||
if _rule.starts_with("RUF9") {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.sorted()
|
||||
.map(Value::String)
|
||||
.collect(),
|
||||
|
||||
@@ -118,8 +118,7 @@ fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
|
||||
let binding = semantic.binding(binding_id);
|
||||
|
||||
let Some(Expr::Call(call)) = analyze::typing::find_binding_value(&name.id, binding, semantic)
|
||||
else {
|
||||
let Some(Expr::Call(call)) = analyze::typing::find_binding_value(binding, semantic) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ mod tests {
|
||||
#[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))]
|
||||
#[test_case(Rule::SuspiciousPickleUsage, Path::new("S301.py"))]
|
||||
#[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))]
|
||||
#[test_case(Rule::SuspiciousMarkSafeUsage, Path::new("S308.py"))]
|
||||
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
|
||||
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
|
||||
#[test_case(Rule::SuspiciousTelnetlibImport, Path::new("S401.py"))]
|
||||
|
||||
@@ -40,7 +40,9 @@ impl Violation for HardcodedBindAllInterfaces {
|
||||
pub(crate) fn hardcoded_bind_all_interfaces(checker: &mut Checker, string: StringLike) {
|
||||
let is_bind_all_interface = match string {
|
||||
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value == "0.0.0.0",
|
||||
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => value == "0.0.0.0",
|
||||
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => {
|
||||
&**value == "0.0.0.0"
|
||||
}
|
||||
StringLike::BytesLiteral(_) => return,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! See: <https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html>
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall};
|
||||
use ruff_python_ast::{self as ast, Decorator, Expr, ExprCall};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -848,7 +848,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
|
||||
// Eval
|
||||
["" | "builtins", "eval"] => Some(SuspiciousEvalUsage.into()),
|
||||
// MarkSafe
|
||||
["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
|
||||
["django", "utils", "safestring" | "html", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
|
||||
// URLOpen (`urlopen`, `urlretrieve`, `Request`)
|
||||
["urllib", "request", "urlopen" | "urlretrieve" | "Request"] |
|
||||
["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" | "Request"] => {
|
||||
@@ -901,3 +901,27 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
/// S308
|
||||
pub(crate) fn suspicious_function_decorator(checker: &mut Checker, decorator: &Decorator) {
|
||||
let Some(diagnostic_kind) = checker
|
||||
.semantic()
|
||||
.resolve_call_path(&decorator.expression)
|
||||
.and_then(|call_path| {
|
||||
match call_path.as_slice() {
|
||||
// MarkSafe
|
||||
["django", "utils", "safestring" | "html", "mark_safe"] => {
|
||||
Some(SuspiciousMarkSafeUsage.into())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let diagnostic = Diagnostic::new::<DiagnosticKind>(diagnostic_kind, decorator.range());
|
||||
if checker.enabled(diagnostic.kind.rule()) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S308.py:5:12: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
|
|
||||
4 | def some_func():
|
||||
5 | return mark_safe('<script>alert("evil!")</script>')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S308
|
||||
|
|
||||
|
||||
S308.py:8:1: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
|
|
||||
8 | @mark_safe
|
||||
| ^^^^^^^^^^ S308
|
||||
9 | def some_func():
|
||||
10 | return '<script>alert("evil!")</script>'
|
||||
|
|
||||
|
||||
S308.py:17:12: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
|
|
||||
16 | def some_func():
|
||||
17 | return mark_safe('<script>alert("evil!")</script>')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S308
|
||||
|
|
||||
|
||||
S308.py:20:1: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
|
|
||||
20 | @mark_safe
|
||||
| ^^^^^^^^^^ S308
|
||||
21 | def some_func():
|
||||
22 | return '<script>alert("evil!")</script>'
|
||||
|
|
||||
|
||||
|
||||
@@ -59,11 +59,11 @@ fn assertion_error(msg: Option<&Expr>) -> Stmt {
|
||||
})),
|
||||
arguments: Arguments {
|
||||
args: if let Some(msg) = msg {
|
||||
vec![msg.clone()]
|
||||
Box::from([msg.clone()])
|
||||
} else {
|
||||
vec![]
|
||||
Box::from([])
|
||||
},
|
||||
keywords: vec![],
|
||||
keywords: Box::from([]),
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
|
||||
@@ -91,7 +91,7 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem])
|
||||
return;
|
||||
}
|
||||
|
||||
let [arg] = arguments.args.as_slice() else {
|
||||
let [arg] = &*arguments.args else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::types::Node;
|
||||
use ruff_python_ast::visitor;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, Arguments, Comprehension, Expr, ExprContext, Stmt};
|
||||
use ruff_python_ast::{self as ast, Comprehension, Expr, ExprContext, Stmt};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -126,18 +126,13 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
|
||||
match expr {
|
||||
Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
arguments:
|
||||
Arguments {
|
||||
args,
|
||||
keywords,
|
||||
range: _,
|
||||
},
|
||||
arguments,
|
||||
range: _,
|
||||
}) => {
|
||||
match func.as_ref() {
|
||||
Expr::Name(ast::ExprName { id, .. }) => {
|
||||
if matches!(id.as_str(), "filter" | "reduce" | "map") {
|
||||
for arg in args {
|
||||
for arg in arguments.args.iter() {
|
||||
if arg.is_lambda_expr() {
|
||||
self.safe_functions.push(arg);
|
||||
}
|
||||
@@ -148,7 +143,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
|
||||
if attr == "reduce" {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() {
|
||||
if id == "functools" {
|
||||
for arg in args {
|
||||
for arg in arguments.args.iter() {
|
||||
if arg.is_lambda_expr() {
|
||||
self.safe_functions.push(arg);
|
||||
}
|
||||
@@ -160,7 +155,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for keyword in keywords {
|
||||
for keyword in arguments.keywords.iter() {
|
||||
if keyword.arg.as_ref().is_some_and(|arg| arg == "key")
|
||||
&& keyword.value.is_lambda_expr()
|
||||
{
|
||||
|
||||
@@ -114,7 +114,7 @@ fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool {
|
||||
}
|
||||
|
||||
// Ex) `iterools.repeat(1, times=None)`
|
||||
for keyword in keywords {
|
||||
for keyword in keywords.iter() {
|
||||
if keyword.arg.as_ref().is_some_and(|name| name == "times") {
|
||||
if keyword.value.is_none_literal_expr() {
|
||||
return true;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Violation};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::lexer::{LexResult, Spanned};
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
@@ -12,20 +10,20 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
/// Simplified token type.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum TokenType {
|
||||
Irrelevant,
|
||||
NonLogicalNewline,
|
||||
Newline,
|
||||
Comma,
|
||||
OpeningBracket,
|
||||
OpeningSquareBracket,
|
||||
OpeningCurlyBracket,
|
||||
ClosingBracket,
|
||||
For,
|
||||
Named,
|
||||
Def,
|
||||
Lambda,
|
||||
Colon,
|
||||
String,
|
||||
Newline,
|
||||
NonLogicalNewline,
|
||||
OpeningBracket,
|
||||
ClosingBracket,
|
||||
OpeningSquareBracket,
|
||||
Colon,
|
||||
Comma,
|
||||
OpeningCurlyBracket,
|
||||
Def,
|
||||
For,
|
||||
Lambda,
|
||||
Irrelevant,
|
||||
}
|
||||
|
||||
/// Simplified token specialized for the task.
|
||||
@@ -54,30 +52,30 @@ impl Token {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Spanned> for Token {
|
||||
fn from(spanned: &Spanned) -> Self {
|
||||
let r#type = match &spanned.0 {
|
||||
Tok::NonLogicalNewline => TokenType::NonLogicalNewline,
|
||||
impl From<(&Tok, TextRange)> for Token {
|
||||
fn from((tok, range): (&Tok, TextRange)) -> Self {
|
||||
let r#type = match tok {
|
||||
Tok::Name { .. } => TokenType::Named,
|
||||
Tok::String { .. } => TokenType::String,
|
||||
Tok::Newline => TokenType::Newline,
|
||||
Tok::For => TokenType::For,
|
||||
Tok::NonLogicalNewline => TokenType::NonLogicalNewline,
|
||||
Tok::Lpar => TokenType::OpeningBracket,
|
||||
Tok::Rpar => TokenType::ClosingBracket,
|
||||
Tok::Lsqb => TokenType::OpeningSquareBracket,
|
||||
Tok::Rsqb => TokenType::ClosingBracket,
|
||||
Tok::Colon => TokenType::Colon,
|
||||
Tok::Comma => TokenType::Comma,
|
||||
Tok::Lbrace => TokenType::OpeningCurlyBracket,
|
||||
Tok::Rbrace => TokenType::ClosingBracket,
|
||||
Tok::Def => TokenType::Def,
|
||||
Tok::For => TokenType::For,
|
||||
Tok::Lambda => TokenType::Lambda,
|
||||
// Import treated like a function.
|
||||
Tok::Import => TokenType::Named,
|
||||
Tok::Name { .. } => TokenType::Named,
|
||||
Tok::String { .. } => TokenType::String,
|
||||
Tok::Comma => TokenType::Comma,
|
||||
Tok::Lpar => TokenType::OpeningBracket,
|
||||
Tok::Lsqb => TokenType::OpeningSquareBracket,
|
||||
Tok::Lbrace => TokenType::OpeningCurlyBracket,
|
||||
Tok::Rpar | Tok::Rsqb | Tok::Rbrace => TokenType::ClosingBracket,
|
||||
Tok::Colon => TokenType::Colon,
|
||||
_ => TokenType::Irrelevant,
|
||||
};
|
||||
Self {
|
||||
range: spanned.1,
|
||||
r#type,
|
||||
}
|
||||
#[allow(clippy::inconsistent_struct_constructor)]
|
||||
Self { range, r#type }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,10 +235,12 @@ pub(crate) fn trailing_commas(
|
||||
indexer: &Indexer,
|
||||
) {
|
||||
let mut fstrings = 0u32;
|
||||
let tokens = tokens
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter_map(|spanned @ (tok, tok_range)| match tok {
|
||||
let tokens = tokens.iter().filter_map(|result| {
|
||||
let Ok((tok, tok_range)) = result else {
|
||||
return None;
|
||||
};
|
||||
|
||||
match tok {
|
||||
// Completely ignore comments -- they just interfere with the logic.
|
||||
Tok::Comment(_) => None,
|
||||
// F-strings are handled as `String` token type with the complete range
|
||||
@@ -263,69 +263,30 @@ pub(crate) fn trailing_commas(
|
||||
}
|
||||
_ => {
|
||||
if fstrings == 0 {
|
||||
Some(Token::from(spanned))
|
||||
Some(Token::from((tok, *tok_range)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
let tokens = [Token::irrelevant(), Token::irrelevant()]
|
||||
.into_iter()
|
||||
.chain(tokens);
|
||||
// Collapse consecutive newlines to the first one -- trailing commas are
|
||||
// added before the first newline.
|
||||
let tokens = tokens.coalesce(|previous, current| {
|
||||
if previous.r#type == TokenType::NonLogicalNewline
|
||||
&& current.r#type == TokenType::NonLogicalNewline
|
||||
{
|
||||
Ok(previous)
|
||||
} else {
|
||||
Err((previous, current))
|
||||
}
|
||||
});
|
||||
|
||||
// The current nesting of the comma contexts.
|
||||
let mut prev = Token::irrelevant();
|
||||
let mut prev_prev = Token::irrelevant();
|
||||
|
||||
let mut stack = vec![Context::new(ContextType::No)];
|
||||
|
||||
for (prev_prev, prev, token) in tokens.tuple_windows() {
|
||||
// Update the comma context stack.
|
||||
match token.r#type {
|
||||
TokenType::OpeningBracket => match (prev.r#type, prev_prev.r#type) {
|
||||
(TokenType::Named, TokenType::Def) => {
|
||||
stack.push(Context::new(ContextType::FunctionParameters));
|
||||
}
|
||||
(TokenType::Named | TokenType::ClosingBracket, _) => {
|
||||
stack.push(Context::new(ContextType::CallArguments));
|
||||
}
|
||||
_ => {
|
||||
stack.push(Context::new(ContextType::Tuple));
|
||||
}
|
||||
},
|
||||
TokenType::OpeningSquareBracket => match prev.r#type {
|
||||
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
|
||||
stack.push(Context::new(ContextType::Subscript));
|
||||
}
|
||||
_ => {
|
||||
stack.push(Context::new(ContextType::List));
|
||||
}
|
||||
},
|
||||
TokenType::OpeningCurlyBracket => {
|
||||
stack.push(Context::new(ContextType::Dict));
|
||||
}
|
||||
TokenType::Lambda => {
|
||||
stack.push(Context::new(ContextType::LambdaParameters));
|
||||
}
|
||||
TokenType::For => {
|
||||
let len = stack.len();
|
||||
stack[len - 1] = Context::new(ContextType::No);
|
||||
}
|
||||
TokenType::Comma => {
|
||||
let len = stack.len();
|
||||
stack[len - 1].inc();
|
||||
}
|
||||
_ => {}
|
||||
for token in tokens {
|
||||
if prev.r#type == TokenType::NonLogicalNewline
|
||||
&& token.r#type == TokenType::NonLogicalNewline
|
||||
{
|
||||
// Collapse consecutive newlines to the first one -- trailing commas are
|
||||
// added before the first newline.
|
||||
continue;
|
||||
}
|
||||
let context = &stack[stack.len() - 1];
|
||||
|
||||
// Update the comma context stack.
|
||||
let context = update_context(token, prev, prev_prev, &mut stack);
|
||||
|
||||
// Is it allowed to have a trailing comma before this token?
|
||||
let comma_allowed = token.r#type == TokenType::ClosingBracket
|
||||
@@ -412,5 +373,47 @@ pub(crate) fn trailing_commas(
|
||||
if pop_context && stack.len() > 1 {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
prev_prev = prev;
|
||||
prev = token;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_context(
|
||||
token: Token,
|
||||
prev: Token,
|
||||
prev_prev: Token,
|
||||
stack: &mut Vec<Context>,
|
||||
) -> Context {
|
||||
let new_context = match token.r#type {
|
||||
TokenType::OpeningBracket => match (prev.r#type, prev_prev.r#type) {
|
||||
(TokenType::Named, TokenType::Def) => Context::new(ContextType::FunctionParameters),
|
||||
(TokenType::Named | TokenType::ClosingBracket, _) => {
|
||||
Context::new(ContextType::CallArguments)
|
||||
}
|
||||
_ => Context::new(ContextType::Tuple),
|
||||
},
|
||||
TokenType::OpeningSquareBracket => match prev.r#type {
|
||||
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
|
||||
Context::new(ContextType::Subscript)
|
||||
}
|
||||
_ => Context::new(ContextType::List),
|
||||
},
|
||||
TokenType::OpeningCurlyBracket => Context::new(ContextType::Dict),
|
||||
TokenType::Lambda => Context::new(ContextType::LambdaParameters),
|
||||
TokenType::For => {
|
||||
let last = stack.last_mut().expect("Stack to never be empty");
|
||||
*last = Context::new(ContextType::No);
|
||||
return *last;
|
||||
}
|
||||
TokenType::Comma => {
|
||||
let last = stack.last_mut().expect("Stack to never be empty");
|
||||
last.inc();
|
||||
return *last;
|
||||
}
|
||||
_ => return stack.last().copied().expect("Stack to never be empty"),
|
||||
};
|
||||
|
||||
stack.push(new_context);
|
||||
new_context
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ use crate::rules::flake8_comprehensions::settings::Settings;
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
|
||||
/// when rewriting the call. In most cases, though, comments will be preserved.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.flake8-comprehensions.allow-dict-calls-with-keyword-arguments`
|
||||
#[violation]
|
||||
pub struct UnnecessaryCollectionCall {
|
||||
obj_type: String,
|
||||
|
||||
@@ -88,7 +88,7 @@ fn is_nullable_field<'a>(value: &'a Expr, semantic: &'a SemanticModel) -> Option
|
||||
let mut null_key = false;
|
||||
let mut blank_key = false;
|
||||
let mut unique_key = false;
|
||||
for keyword in &call.arguments.keywords {
|
||||
for keyword in call.arguments.keywords.iter() {
|
||||
let Some(argument) = &keyword.arg else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -113,7 +113,7 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) {
|
||||
.resolve_call_path(func)
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["", "dict"]))
|
||||
{
|
||||
for keyword in keywords {
|
||||
for keyword in keywords.iter() {
|
||||
if let Some(attr) = &keyword.arg {
|
||||
if is_reserved_attr(attr) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
|
||||
@@ -97,7 +97,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let [arg] = args.as_slice() else {
|
||||
let [arg] = &**args else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -188,8 +188,8 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
|
||||
let node3 = Expr::Call(ast::ExprCall {
|
||||
func: Box::new(node2),
|
||||
arguments: Arguments {
|
||||
args: vec![node],
|
||||
keywords: vec![],
|
||||
args: Box::from([node]),
|
||||
keywords: Box::from([]),
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
|
||||
@@ -59,7 +59,7 @@ impl Violation for UnnecessaryDictKwargs {
|
||||
/// PIE804
|
||||
pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let mut duplicate_keywords = None;
|
||||
for keyword in &call.arguments.keywords {
|
||||
for keyword in call.arguments.keywords.iter() {
|
||||
// keyword is a spread operator (indicated by None).
|
||||
if keyword.arg.is_some() {
|
||||
continue;
|
||||
@@ -145,7 +145,7 @@ fn duplicates(call: &ast::ExprCall) -> FxHashSet<&str> {
|
||||
call.arguments.keywords.len(),
|
||||
BuildHasherDefault::default(),
|
||||
);
|
||||
for keyword in &call.arguments.keywords {
|
||||
for keyword in call.arguments.keywords.iter() {
|
||||
if let Some(name) = &keyword.arg {
|
||||
if !seen.insert(name.as_str()) {
|
||||
duplicates.insert(name.as_str());
|
||||
|
||||
@@ -60,7 +60,7 @@ pub(crate) fn unnecessary_range_start(checker: &mut Checker, call: &ast::ExprCal
|
||||
}
|
||||
|
||||
// Verify that the call has exactly two arguments (no `step`).
|
||||
let [start, _] = call.arguments.args.as_slice() else {
|
||||
let [start, _] = &*call.arguments.args else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ pub(crate) fn bad_version_info_comparison(checker: &mut Checker, test: &Expr) {
|
||||
return;
|
||||
};
|
||||
|
||||
let ([op], [_right]) = (ops.as_slice(), comparators.as_slice()) else {
|
||||
let ([op], [_right]) = (&**ops, &**comparators) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ pub(crate) fn unrecognized_platform(checker: &mut Checker, test: &Expr) {
|
||||
return;
|
||||
};
|
||||
|
||||
let ([op], [right]) = (ops.as_slice(), comparators.as_slice()) else {
|
||||
let ([op], [right]) = (&**ops, &**comparators) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ pub(crate) fn unrecognized_version_info(checker: &mut Checker, test: &Expr) {
|
||||
return;
|
||||
};
|
||||
|
||||
let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) else {
|
||||
let ([op], [comparator]) = (&**ops, &**comparators) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -15,13 +15,11 @@ PYI049.py:9:7: PYI049 Private TypedDict `_UnusedTypedDict2` is never used
|
||||
10 | bar: int
|
||||
|
|
||||
|
||||
PYI049.py:20:1: PYI049 Private TypedDict `_UnusedTypedDict3` is never used
|
||||
PYI049.py:21:1: PYI049 Private TypedDict `_UnusedTypedDict3` is never used
|
||||
|
|
||||
18 | bar: list[int]
|
||||
19 |
|
||||
20 | _UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int})
|
||||
21 | _UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int})
|
||||
| ^^^^^^^^^^^^^^^^^ PYI049
|
||||
21 | _UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes})
|
||||
22 | _UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes})
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -24,4 +24,13 @@ PYI049.pyi:34:1: PYI049 Private TypedDict `_UnusedTypedDict3` is never used
|
||||
35 | _UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes})
|
||||
|
|
||||
|
||||
PYI049.pyi:43:11: PYI049 Private TypedDict `_UnusedTypeDict4` is never used
|
||||
|
|
||||
41 | # scope (unlike in `.py` files).
|
||||
42 | class _CustomClass3:
|
||||
43 | class _UnusedTypeDict4(TypedDict):
|
||||
| ^^^^^^^^^^^^^^^^ PYI049
|
||||
44 | pass
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -411,7 +411,7 @@ fn to_pytest_raises_args<'a>(
|
||||
) -> Option<Cow<'a, str>> {
|
||||
let args = match attr {
|
||||
"assertRaises" | "failUnlessRaises" => {
|
||||
match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
|
||||
match (&*arguments.args, &*arguments.keywords) {
|
||||
// Ex) `assertRaises(Exception)`
|
||||
([arg], []) => Cow::Borrowed(checker.locator().slice(arg)),
|
||||
// Ex) `assertRaises(expected_exception=Exception)`
|
||||
@@ -427,7 +427,7 @@ fn to_pytest_raises_args<'a>(
|
||||
}
|
||||
}
|
||||
"assertRaisesRegex" | "assertRaisesRegexp" => {
|
||||
match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
|
||||
match (&*arguments.args, &*arguments.keywords) {
|
||||
// Ex) `assertRaisesRegex(Exception, regex)`
|
||||
([arg1, arg2], []) => Cow::Owned(format!(
|
||||
"{}, match={}",
|
||||
|
||||
@@ -257,15 +257,18 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
|
||||
}
|
||||
|
||||
let node = Expr::from(ast::StringLiteral {
|
||||
value: elts.iter().fold(String::new(), |mut acc, elt| {
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = elt {
|
||||
if !acc.is_empty() {
|
||||
acc.push(',');
|
||||
value: elts
|
||||
.iter()
|
||||
.fold(String::new(), |mut acc, elt| {
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = elt {
|
||||
if !acc.is_empty() {
|
||||
acc.push(',');
|
||||
}
|
||||
acc.push_str(value.to_str());
|
||||
}
|
||||
acc.push_str(value.to_str());
|
||||
}
|
||||
acc
|
||||
}),
|
||||
acc
|
||||
})
|
||||
.into_boxed_str(),
|
||||
..ast::StringLiteral::default()
|
||||
});
|
||||
Some(generator.expr(&node))
|
||||
@@ -327,7 +330,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
|
||||
.iter()
|
||||
.map(|name| {
|
||||
Expr::from(ast::StringLiteral {
|
||||
value: (*name).to_string(),
|
||||
value: (*name).to_string().into_boxed_str(),
|
||||
..ast::StringLiteral::default()
|
||||
})
|
||||
})
|
||||
@@ -360,7 +363,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
|
||||
.iter()
|
||||
.map(|name| {
|
||||
Expr::from(ast::StringLiteral {
|
||||
value: (*name).to_string(),
|
||||
value: (*name).to_string().into_boxed_str(),
|
||||
..ast::StringLiteral::default()
|
||||
})
|
||||
})
|
||||
@@ -635,17 +638,17 @@ pub(crate) fn parametrize(checker: &mut Checker, decorators: &[Decorator]) {
|
||||
}) = &decorator.expression
|
||||
{
|
||||
if checker.enabled(Rule::PytestParametrizeNamesWrongType) {
|
||||
if let [names, ..] = args.as_slice() {
|
||||
if let [names, ..] = &**args {
|
||||
check_names(checker, decorator, names);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::PytestParametrizeValuesWrongType) {
|
||||
if let [names, values, ..] = args.as_slice() {
|
||||
if let [names, values, ..] = &**args {
|
||||
check_values(checker, names, values);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::PytestDuplicateParametrizeTestCases) {
|
||||
if let [_, values, ..] = args.as_slice() {
|
||||
if let [_, values, ..] = &**args {
|
||||
check_duplicates(checker, values);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,8 +173,8 @@ fn assert(expr: &Expr, msg: Option<&Expr>) -> Stmt {
|
||||
fn compare(left: &Expr, cmp_op: CmpOp, right: &Expr) -> Expr {
|
||||
Expr::Compare(ast::ExprCompare {
|
||||
left: Box::new(left.clone()),
|
||||
ops: vec![cmp_op],
|
||||
comparators: vec![right.clone()],
|
||||
ops: Box::from([cmp_op]),
|
||||
comparators: Box::from([right.clone()]),
|
||||
range: TextRange::default(),
|
||||
})
|
||||
}
|
||||
@@ -390,8 +390,8 @@ impl UnittestAssert {
|
||||
let node1 = ast::ExprCall {
|
||||
func: Box::new(node.into()),
|
||||
arguments: Arguments {
|
||||
args: vec![(**obj).clone(), (**cls).clone()],
|
||||
keywords: vec![],
|
||||
args: Box::from([(**obj).clone(), (**cls).clone()]),
|
||||
keywords: Box::from([]),
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
@@ -434,8 +434,8 @@ impl UnittestAssert {
|
||||
let node2 = ast::ExprCall {
|
||||
func: Box::new(node1.into()),
|
||||
arguments: Arguments {
|
||||
args: vec![(**regex).clone(), (**text).clone()],
|
||||
keywords: vec![],
|
||||
args: Box::from([(**regex).clone(), (**text).clone()]),
|
||||
keywords: Box::from([]),
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
|
||||
@@ -221,6 +221,7 @@ pub(crate) fn avoidable_escaped_quote(
|
||||
Tok::FStringMiddle {
|
||||
value: string_contents,
|
||||
is_raw,
|
||||
triple_quoted: _,
|
||||
} if !is_raw => {
|
||||
let Some(context) = fstrings.last_mut() else {
|
||||
continue;
|
||||
@@ -361,6 +362,7 @@ pub(crate) fn unnecessary_escaped_quote(
|
||||
Tok::FStringMiddle {
|
||||
value: string_contents,
|
||||
is_raw,
|
||||
triple_quoted: _,
|
||||
} if !is_raw => {
|
||||
let Some(context) = fstrings.last_mut() else {
|
||||
continue;
|
||||
|
||||
@@ -437,8 +437,8 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) {
|
||||
let node2 = ast::ExprCall {
|
||||
func: Box::new(node1.into()),
|
||||
arguments: Arguments {
|
||||
args: vec![target.clone(), node.into()],
|
||||
keywords: vec![],
|
||||
args: Box::from([target.clone(), node.into()]),
|
||||
keywords: Box::from([]),
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
@@ -480,13 +480,13 @@ fn match_eq_target(expr: &Expr) -> Option<(&str, &Expr)> {
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if ops != &[CmpOp::Eq] {
|
||||
if **ops != [CmpOp::Eq] {
|
||||
return None;
|
||||
}
|
||||
let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
let [comparator] = comparators.as_slice() else {
|
||||
let [comparator] = &**comparators else {
|
||||
return None;
|
||||
};
|
||||
if !comparator.is_name_expr() {
|
||||
@@ -551,8 +551,8 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) {
|
||||
};
|
||||
let node2 = ast::ExprCompare {
|
||||
left: Box::new(node1.into()),
|
||||
ops: vec![CmpOp::In],
|
||||
comparators: vec![node.into()],
|
||||
ops: Box::from([CmpOp::In]),
|
||||
comparators: Box::from([node.into()]),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
let in_expr = node2.into();
|
||||
|
||||
@@ -217,7 +217,7 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) {
|
||||
slice.range(),
|
||||
);
|
||||
let node = ast::StringLiteral {
|
||||
value: capital_env_var,
|
||||
value: capital_env_var.into_boxed_str(),
|
||||
unicode: env_var.is_unicode(),
|
||||
..ast::StringLiteral::default()
|
||||
};
|
||||
|
||||
@@ -185,8 +185,8 @@ pub(crate) fn if_expr_with_true_false(
|
||||
.into(),
|
||||
),
|
||||
arguments: Arguments {
|
||||
args: vec![test.clone()],
|
||||
keywords: vec![],
|
||||
args: Box::from([test.clone()]),
|
||||
keywords: Box::from([]),
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
|
||||
@@ -176,7 +176,7 @@ pub(crate) fn negation_with_equal_op(
|
||||
);
|
||||
let node = ast::ExprCompare {
|
||||
left: left.clone(),
|
||||
ops: vec![CmpOp::NotEq],
|
||||
ops: Box::from([CmpOp::NotEq]),
|
||||
comparators: comparators.clone(),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
@@ -206,7 +206,7 @@ pub(crate) fn negation_with_not_equal_op(
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !matches!(&ops[..], [CmpOp::NotEq]) {
|
||||
if !matches!(&**ops, [CmpOp::NotEq]) {
|
||||
return;
|
||||
}
|
||||
if is_exception_check(checker.semantic().current_statement()) {
|
||||
@@ -231,7 +231,7 @@ pub(crate) fn negation_with_not_equal_op(
|
||||
);
|
||||
let node = ast::ExprCompare {
|
||||
left: left.clone(),
|
||||
ops: vec![CmpOp::Eq],
|
||||
ops: Box::from([CmpOp::Eq]),
|
||||
comparators: comparators.clone(),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
@@ -279,8 +279,8 @@ pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: UnaryOp, o
|
||||
let node1 = ast::ExprCall {
|
||||
func: Box::new(node.into()),
|
||||
arguments: Arguments {
|
||||
args: vec![*operand.clone()],
|
||||
keywords: vec![],
|
||||
args: Box::from([*operand.clone()]),
|
||||
keywords: Box::from([]),
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
|
||||
@@ -253,8 +253,7 @@ fn is_main_check(expr: &Expr) -> bool {
|
||||
{
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() {
|
||||
if id == "__name__" {
|
||||
if let [Expr::StringLiteral(ast::ExprStringLiteral { value, .. })] =
|
||||
comparators.as_slice()
|
||||
if let [Expr::StringLiteral(ast::ExprStringLiteral { value, .. })] = &**comparators
|
||||
{
|
||||
if value == "__main__" {
|
||||
return true;
|
||||
|
||||
@@ -76,8 +76,7 @@ pub(crate) fn enumerate_for_loop(checker: &mut Checker, for_stmt: &ast::StmtFor)
|
||||
}
|
||||
|
||||
// Ensure that the index variable was initialized to 0.
|
||||
let Some(value) = typing::find_binding_value(&index.id, binding, checker.semantic())
|
||||
else {
|
||||
let Some(value) = typing::find_binding_value(binding, checker.semantic()) else {
|
||||
continue;
|
||||
};
|
||||
if !matches!(
|
||||
|
||||
@@ -122,7 +122,7 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if:
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let [test_dict] = test_dict.as_slice() else {
|
||||
let [test_dict] = &**test_dict else {
|
||||
return;
|
||||
};
|
||||
let (expected_var, expected_value, default_var, default_value) = match ops[..] {
|
||||
@@ -176,8 +176,8 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if:
|
||||
let node3 = ast::ExprCall {
|
||||
func: Box::new(node2.into()),
|
||||
arguments: Arguments {
|
||||
args: vec![node1, node],
|
||||
keywords: vec![],
|
||||
args: Box::from([node1, node]),
|
||||
keywords: Box::from([]),
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
@@ -233,11 +233,11 @@ pub(crate) fn if_exp_instead_of_dict_get(
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let [test_dict] = test_dict.as_slice() else {
|
||||
let [test_dict] = &**test_dict else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (body, default_value) = match ops.as_slice() {
|
||||
let (body, default_value) = match &**ops {
|
||||
[CmpOp::In] => (body, orelse),
|
||||
[CmpOp::NotIn] => (orelse, body),
|
||||
_ => {
|
||||
@@ -276,8 +276,8 @@ pub(crate) fn if_exp_instead_of_dict_get(
|
||||
let fixed_node = ast::ExprCall {
|
||||
func: Box::new(dict_get_node.into()),
|
||||
arguments: Arguments {
|
||||
args: vec![dict_key_node, default_value_node],
|
||||
keywords: vec![],
|
||||
args: Box::from([dict_key_node, default_value_node]),
|
||||
keywords: Box::from([]),
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
|
||||
@@ -64,10 +64,10 @@ pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &mut Checker, stmt_i
|
||||
let Expr::Name(ast::ExprName { id: target, .. }) = left.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if ops != &[CmpOp::Eq] {
|
||||
if **ops != [CmpOp::Eq] {
|
||||
return;
|
||||
}
|
||||
let [expr] = comparators.as_slice() else {
|
||||
let [expr] = &**comparators else {
|
||||
return;
|
||||
};
|
||||
let Some(literal_expr) = expr.as_literal_expr() else {
|
||||
@@ -127,10 +127,10 @@ pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &mut Checker, stmt_i
|
||||
let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if id != target || ops != &[CmpOp::Eq] {
|
||||
if id != target || **ops != [CmpOp::Eq] {
|
||||
return;
|
||||
}
|
||||
let [expr] = comparators.as_slice() else {
|
||||
let [expr] = &**comparators else {
|
||||
return;
|
||||
};
|
||||
let Some(literal_expr) = expr.as_literal_expr() else {
|
||||
|
||||
@@ -194,7 +194,7 @@ pub(crate) fn key_in_dict_comprehension(checker: &mut Checker, comprehension: &C
|
||||
|
||||
/// SIM118 in a comparison.
|
||||
pub(crate) fn key_in_dict_compare(checker: &mut Checker, compare: &ast::ExprCompare) {
|
||||
let [op] = compare.ops.as_slice() else {
|
||||
let [op] = &*compare.ops else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -202,7 +202,7 @@ pub(crate) fn key_in_dict_compare(checker: &mut Checker, compare: &ast::ExprComp
|
||||
return;
|
||||
}
|
||||
|
||||
let [right] = compare.comparators.as_slice() else {
|
||||
let [right] = &*compare.comparators else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -161,8 +161,8 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
let value_node = ast::ExprCall {
|
||||
func: Box::new(func_node.into()),
|
||||
arguments: Arguments {
|
||||
args: vec![if_test.clone()],
|
||||
keywords: vec![],
|
||||
args: Box::from([if_test.clone()]),
|
||||
keywords: Box::from([]),
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
|
||||
@@ -140,7 +140,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
|
||||
range: _,
|
||||
}) = &loop_.test
|
||||
{
|
||||
if let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) {
|
||||
if let ([op], [comparator]) = (&**ops, &**comparators) {
|
||||
let op = match op {
|
||||
CmpOp::Eq => CmpOp::NotEq,
|
||||
CmpOp::NotEq => CmpOp::Eq,
|
||||
@@ -155,8 +155,8 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
|
||||
};
|
||||
let node = ast::ExprCompare {
|
||||
left: left.clone(),
|
||||
ops: vec![op],
|
||||
comparators: vec![comparator.clone()],
|
||||
ops: Box::from([op]),
|
||||
comparators: Box::from([comparator.clone()]),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
node.into()
|
||||
@@ -391,8 +391,8 @@ fn return_stmt(id: &str, test: &Expr, target: &Expr, iter: &Expr, generator: Gen
|
||||
let node2 = ast::ExprCall {
|
||||
func: Box::new(node1.into()),
|
||||
arguments: Arguments {
|
||||
args: vec![node.into()],
|
||||
keywords: vec![],
|
||||
args: Box::from([node.into()]),
|
||||
keywords: Box::from([]),
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
|
||||
@@ -3,24 +3,20 @@ source: crates/ruff_linter/src/rules/flake8_trio/mod.rs
|
||||
---
|
||||
TRIO100.py:5:5: TRIO100 A `with trio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
4 | async def foo():
|
||||
4 | async def func():
|
||||
5 | with trio.fail_after():
|
||||
| _____^
|
||||
6 | | ...
|
||||
| |___________^ TRIO100
|
||||
7 |
|
||||
8 | async def foo():
|
||||
|
|
||||
|
||||
TRIO100.py:13:5: TRIO100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
TRIO100.py:15:5: TRIO100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
12 | async def foo():
|
||||
13 | with trio.move_on_after():
|
||||
14 | async def func():
|
||||
15 | with trio.move_on_after():
|
||||
| _____^
|
||||
14 | | ...
|
||||
16 | | ...
|
||||
| |___________^ TRIO100
|
||||
15 |
|
||||
16 | async def foo():
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
///
|
||||
/// When possible, using `Path` object methods such as `Path.stat()` can
|
||||
/// improve readability over the `os` module's counterparts (e.g.,
|
||||
/// `os.path.getsize()`).
|
||||
/// `os.path.getatime()`).
|
||||
///
|
||||
/// Note that `os` functions may be preferable if performance is a concern,
|
||||
/// e.g., in hot loops.
|
||||
@@ -19,19 +19,19 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.path.getsize(__file__)
|
||||
/// os.path.getatime(__file__)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path(__file__).stat().st_size
|
||||
/// Path(__file__).stat().st_atime
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
|
||||
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
|
||||
/// - [Python documentation: `os.path.getatime`](https://docs.python.org/3/library/os.path.html#os.path.getatime)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
|
||||
@@ -2,7 +2,7 @@ use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.getatime`.
|
||||
/// Checks for uses of `os.path.getctime`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
@@ -10,7 +10,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
///
|
||||
/// When possible, using `Path` object methods such as `Path.stat()` can
|
||||
/// improve readability over the `os` module's counterparts (e.g.,
|
||||
/// `os.path.getsize()`).
|
||||
/// `os.path.getctime()`).
|
||||
///
|
||||
/// Note that `os` functions may be preferable if performance is a concern,
|
||||
/// e.g., in hot loops.
|
||||
@@ -19,19 +19,19 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.path.getsize(__file__)
|
||||
/// os.path.getctime(__file__)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path(__file__).stat().st_size
|
||||
/// Path(__file__).stat().st_ctime
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
|
||||
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
|
||||
/// - [Python documentation: `os.path.getctime`](https://docs.python.org/3/library/os.path.html#os.path.getctime)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
|
||||
@@ -2,7 +2,7 @@ use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.getatime`.
|
||||
/// Checks for uses of `os.path.getmtime`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
@@ -10,7 +10,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
///
|
||||
/// When possible, using `Path` object methods such as `Path.stat()` can
|
||||
/// improve readability over the `os` module's counterparts (e.g.,
|
||||
/// `os.path.getsize()`).
|
||||
/// `os.path.getmtime()`).
|
||||
///
|
||||
/// Note that `os` functions may be preferable if performance is a concern,
|
||||
/// e.g., in hot loops.
|
||||
@@ -19,19 +19,19 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.path.getsize(__file__)
|
||||
/// os.path.getmtime(__file__)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path(__file__).stat().st_size
|
||||
/// Path(__file__).stat().st_mtime
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
|
||||
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
|
||||
/// - [Python documentation: `os.path.getmtime`](https://docs.python.org/3/library/os.path.html#os.path.getmtime)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user