Compare commits
40 Commits
range-form
...
col-row-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5be2ec5ced | ||
|
|
59c37d0cd8 | ||
|
|
f39e8af8ae | ||
|
|
dd8b1244fd | ||
|
|
93b5d8a0fb | ||
|
|
65aebf127a | ||
|
|
17ceb5dcb3 | ||
|
|
8ce138760a | ||
|
|
10e35e38d7 | ||
|
|
f169cb5d92 | ||
|
|
39ddad7454 | ||
|
|
f32b0eef9c | ||
|
|
15813a65f3 | ||
|
|
604cf521b5 | ||
|
|
865c89800e | ||
|
|
1a22eae98c | ||
|
|
8ba8896a7f | ||
|
|
b194f59aab | ||
|
|
e41b08f1d0 | ||
|
|
1a4f2a9baf | ||
|
|
19010f276e | ||
|
|
5174e8c926 | ||
|
|
8bfe9bda41 | ||
|
|
01843af21a | ||
|
|
2ecf59726f | ||
|
|
f137819536 | ||
|
|
9d16e46129 | ||
|
|
82978ac9b5 | ||
|
|
814403cdf7 | ||
|
|
f254aaa847 | ||
|
|
a51b0b02f0 | ||
|
|
74dbd871f8 | ||
|
|
d7508af48d | ||
|
|
c3774e1255 | ||
|
|
887455c498 | ||
|
|
4d6f5ff0a7 | ||
|
|
6c3378edb1 | ||
|
|
7f1456a2c9 | ||
|
|
2759db6604 | ||
|
|
124d95d246 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -208,3 +208,9 @@ cython_debug/
|
||||
# VIM
|
||||
.*.sw?
|
||||
.sw?
|
||||
|
||||
# Custom re-inclusions for the resolver test cases
|
||||
!crates/ruff_python_resolver/resources/test/airflow/venv/
|
||||
!crates/ruff_python_resolver/resources/test/airflow/venv/lib
|
||||
!crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so
|
||||
!crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so
|
||||
|
||||
50
Cargo.lock
generated
50
Cargo.lock
generated
@@ -810,7 +810,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.290"
|
||||
version = "0.0.291"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1035,9 +1035,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.17.6"
|
||||
version = "0.17.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b297dc40733f23a0e52728a58fa9489a5b7638a324932de16b41adc3ef80730"
|
||||
checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25"
|
||||
dependencies = [
|
||||
"console",
|
||||
"instant",
|
||||
@@ -1454,27 +1454,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.16"
|
||||
@@ -2051,7 +2030,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_cli"
|
||||
version = "0.0.290"
|
||||
version = "0.0.291"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -2187,7 +2166,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.0.290"
|
||||
version = "0.0.291"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -2206,8 +2185,6 @@ dependencies = [
|
||||
"log",
|
||||
"memchr",
|
||||
"natord",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"path-absolutize",
|
||||
"pathdiff",
|
||||
@@ -2290,8 +2267,6 @@ dependencies = [
|
||||
"is-macro",
|
||||
"itertools 0.11.0",
|
||||
"memchr",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_trivia",
|
||||
@@ -2336,6 +2311,7 @@ dependencies = [
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"similar",
|
||||
@@ -2367,7 +2343,6 @@ dependencies = [
|
||||
"is-macro",
|
||||
"itertools 0.11.0",
|
||||
"lexical-parse-float",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"unic-ucd-category",
|
||||
]
|
||||
@@ -2382,8 +2357,6 @@ dependencies = [
|
||||
"itertools 0.11.0",
|
||||
"lalrpop",
|
||||
"lalrpop-util",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"ruff_python_ast",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
@@ -2409,7 +2382,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"is-macro",
|
||||
"num-traits",
|
||||
"ruff_index",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
@@ -2523,7 +2495,9 @@ dependencies = [
|
||||
"ruff_formatter",
|
||||
"ruff_linter",
|
||||
"ruff_macros",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_source_file",
|
||||
"rustc-hash",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -2669,9 +2643,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.18"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
|
||||
checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
@@ -3516,9 +3490,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wild"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05b116685a6be0c52f5a103334cbff26db643826c7b3735fc0a3ba9871310a74"
|
||||
checksum = "10d01931a94d5a115a53f95292f51d316856b68a035618eb831bbba593a30b67"
|
||||
dependencies = [
|
||||
"glob",
|
||||
]
|
||||
|
||||
@@ -26,8 +26,6 @@ is-macro = { version = "0.3.0" }
|
||||
itertools = { version = "0.11.0" }
|
||||
log = { version = "0.4.17" }
|
||||
memchr = "2.6.3"
|
||||
num-bigint = { version = "0.4.3" }
|
||||
num-traits = { version = "0.2.15" }
|
||||
once_cell = { version = "1.17.1" }
|
||||
path-absolutize = { version = "3.1.1" }
|
||||
proc-macro2 = { version = "1.0.67" }
|
||||
|
||||
@@ -30,7 +30,7 @@ An extremely fast Python linter, written in Rust.
|
||||
- 🤝 Python 3.11 compatibility
|
||||
- 📦 Built-in caching, to avoid re-analyzing unchanged files
|
||||
- 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
|
||||
- 📏 Over [600 built-in rules](https://docs.astral.sh/ruff/rules/)
|
||||
- 📏 Over [700 built-in rules](https://docs.astral.sh/ruff/rules/)
|
||||
- ⚖️ [Near-parity](https://docs.astral.sh/ruff/faq/#how-does-ruff-compare-to-flake8) with the
|
||||
built-in Flake8 rule set
|
||||
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
|
||||
@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.0.290
|
||||
rev: v0.0.291
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
@@ -233,7 +233,7 @@ linting command.
|
||||
|
||||
<!-- Begin section: Rules -->
|
||||
|
||||
**Ruff supports over 600 lint rules**, many of which are inspired by popular tools like Flake8,
|
||||
**Ruff supports over 700 lint rules**, many of which are inspired by popular tools like Flake8,
|
||||
isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implements every rule in
|
||||
Rust as a first-party feature.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.290"
|
||||
version = "0.0.291"
|
||||
description = """
|
||||
Convert Flake8 configuration files to Ruff configuration files.
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,7 @@ use ruff_benchmark::criterion::{
|
||||
criterion_group, criterion_main, BenchmarkId, Criterion, Throughput,
|
||||
};
|
||||
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
|
||||
use ruff_python_formatter::{format_node, PyFormatOptions};
|
||||
use ruff_python_formatter::{format_module_ast, PyFormatOptions};
|
||||
use ruff_python_index::CommentRangesBuilder;
|
||||
use ruff_python_parser::lexer::lex;
|
||||
use ruff_python_parser::{parse_tokens, Mode};
|
||||
@@ -65,13 +65,14 @@ fn benchmark_formatter(criterion: &mut Criterion) {
|
||||
let comment_ranges = comment_ranges.finish();
|
||||
|
||||
// Parse the AST.
|
||||
let python_ast = parse_tokens(tokens, Mode::Module, "<filename>")
|
||||
let module = parse_tokens(tokens, Mode::Module, "<filename>")
|
||||
.expect("Input to be a valid python program");
|
||||
|
||||
b.iter(|| {
|
||||
let options = PyFormatOptions::from_extension(Path::new(case.name()));
|
||||
let formatted = format_node(&python_ast, &comment_ranges, case.code(), options)
|
||||
.expect("Formatting to succeed");
|
||||
let formatted =
|
||||
format_module_ast(&module, &comment_ranges, case.code(), options)
|
||||
.expect("Formatting to succeed");
|
||||
|
||||
formatted.print().expect("Printing to succeed")
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_cli"
|
||||
version = "0.0.290"
|
||||
version = "0.0.291"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
a = 1
|
||||
|
||||
__all__ = list(["a", "b"])
|
||||
@@ -1,12 +1,13 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::OptionsMetadata;
|
||||
|
||||
#[allow(clippy::print_stdout)]
|
||||
pub(crate) fn config(key: Option<&str>) -> Result<()> {
|
||||
match key {
|
||||
None => print!("{}", Options::metadata()),
|
||||
Some(key) => match Options::metadata().get(key) {
|
||||
Some(key) => match Options::metadata().find(key) {
|
||||
None => {
|
||||
return Err(anyhow!("Unknown option: {key}"));
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ use ruff_linter::fs;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::warn_user_once;
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_python_formatter::{format_module, FormatModuleError, PyFormatOptions};
|
||||
use ruff_source_file::{find_newline, LineEnding};
|
||||
use ruff_python_formatter::{format_module_source, FormatModuleError};
|
||||
use ruff_workspace::resolver::python_files_in_path;
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments};
|
||||
use crate::panic::{catch_unwind, PanicError};
|
||||
@@ -73,15 +73,17 @@ pub(crate) fn format(
|
||||
};
|
||||
|
||||
let resolved_settings = resolver.resolve(path, &pyproject_config);
|
||||
let options = resolved_settings.formatter.to_format_options(source_type);
|
||||
debug!("Formatting {} with {:?}", path.display(), options);
|
||||
|
||||
Some(match catch_unwind(|| format_path(path, options, mode)) {
|
||||
Ok(inner) => inner,
|
||||
Err(error) => {
|
||||
Err(FormatCommandError::Panic(Some(path.to_path_buf()), error))
|
||||
}
|
||||
})
|
||||
Some(
|
||||
match catch_unwind(|| {
|
||||
format_path(path, &resolved_settings.formatter, source_type, mode)
|
||||
}) {
|
||||
Ok(inner) => inner,
|
||||
Err(error) => {
|
||||
Err(FormatCommandError::Panic(Some(path.to_path_buf()), error))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Err(err) => Some(Err(FormatCommandError::Ignore(err))),
|
||||
}
|
||||
@@ -139,21 +141,17 @@ pub(crate) fn format(
|
||||
#[tracing::instrument(skip_all, fields(path = %path.display()))]
|
||||
fn format_path(
|
||||
path: &Path,
|
||||
options: PyFormatOptions,
|
||||
settings: &FormatterSettings,
|
||||
source_type: PySourceType,
|
||||
mode: FormatMode,
|
||||
) -> Result<FormatCommandResult, FormatCommandError> {
|
||||
let unformatted = std::fs::read_to_string(path)
|
||||
.map_err(|err| FormatCommandError::Read(Some(path.to_path_buf()), err))?;
|
||||
|
||||
let line_ending = match find_newline(&unformatted) {
|
||||
Some((_, LineEnding::Lf)) | None => ruff_formatter::printer::LineEnding::LineFeed,
|
||||
Some((_, LineEnding::Cr)) => ruff_formatter::printer::LineEnding::CarriageReturn,
|
||||
Some((_, LineEnding::CrLf)) => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed,
|
||||
};
|
||||
let options = settings.to_format_options(source_type, &unformatted);
|
||||
debug!("Formatting {} with {:?}", path.display(), options);
|
||||
|
||||
let options = options.with_line_ending(line_ending);
|
||||
|
||||
let formatted = format_module(&unformatted, options)
|
||||
let formatted = format_module_source(&unformatted, options)
|
||||
.map_err(|err| FormatCommandError::FormatModule(Some(path.to_path_buf()), err))?;
|
||||
|
||||
let formatted = formatted.as_code();
|
||||
|
||||
@@ -5,8 +5,9 @@ use anyhow::Result;
|
||||
use log::warn;
|
||||
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_formatter::{format_module, PyFormatOptions};
|
||||
use ruff_python_formatter::format_module_source;
|
||||
use ruff_workspace::resolver::python_file_at_path;
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments};
|
||||
use crate::commands::format::{FormatCommandError, FormatCommandResult, FormatMode};
|
||||
@@ -37,12 +38,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||
// Format the file.
|
||||
let path = cli.stdin_filename.as_deref();
|
||||
|
||||
let options = pyproject_config
|
||||
.settings
|
||||
.formatter
|
||||
.to_format_options(path.map(PySourceType::from).unwrap_or_default());
|
||||
|
||||
match format_source(path, options, mode) {
|
||||
match format_source(path, &pyproject_config.settings.formatter, mode) {
|
||||
Ok(result) => match mode {
|
||||
FormatMode::Write => Ok(ExitStatus::Success),
|
||||
FormatMode::Check => {
|
||||
@@ -63,23 +59,30 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||
/// Format source code read from `stdin`.
|
||||
fn format_source(
|
||||
path: Option<&Path>,
|
||||
options: PyFormatOptions,
|
||||
settings: &FormatterSettings,
|
||||
mode: FormatMode,
|
||||
) -> Result<FormatCommandResult, FormatCommandError> {
|
||||
let unformatted = read_from_stdin()
|
||||
.map_err(|err| FormatCommandError::Read(path.map(Path::to_path_buf), err))?;
|
||||
let formatted = format_module(&unformatted, options)
|
||||
|
||||
let options = settings.to_format_options(
|
||||
path.map(PySourceType::from).unwrap_or_default(),
|
||||
&unformatted,
|
||||
);
|
||||
|
||||
let formatted = format_module_source(&unformatted, options)
|
||||
.map_err(|err| FormatCommandError::FormatModule(path.map(Path::to_path_buf), err))?;
|
||||
let formatted = formatted.as_code();
|
||||
|
||||
if mode.is_write() {
|
||||
stdout()
|
||||
.lock()
|
||||
.write_all(formatted.as_bytes())
|
||||
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;
|
||||
}
|
||||
if formatted.len() == unformatted.len() && formatted == unformatted {
|
||||
Ok(FormatCommandResult::Unchanged)
|
||||
} else {
|
||||
if mode.is_write() {
|
||||
stdout()
|
||||
.lock()
|
||||
.write_all(formatted.as_bytes())
|
||||
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;
|
||||
}
|
||||
Ok(FormatCommandResult::Formatted)
|
||||
}
|
||||
}
|
||||
|
||||
207
crates/ruff_cli/tests/format.rs
Normal file
207
crates/ruff_cli/tests/format.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
#![cfg(not(target_family = "wasm"))]
|
||||
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use std::str;
|
||||
|
||||
use anyhow::Result;
|
||||
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||
use tempfile::TempDir;
|
||||
|
||||
const BIN_NAME: &str = "ruff";
|
||||
|
||||
#[test]
|
||||
fn default_options() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print('Should\'t change quotes')
|
||||
|
||||
|
||||
if condition:
|
||||
|
||||
print('Hy "Micha"') # Should not change quotes
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
def foo(
|
||||
arg1,
|
||||
arg2,
|
||||
):
|
||||
print("Should't change quotes")
|
||||
|
||||
|
||||
if condition:
|
||||
print('Hy "Micha"') # Should not change quotes
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_options() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[format]
|
||||
indent-style = "tab"
|
||||
quote-style = "single"
|
||||
skip-magic-trailing-comma = true
|
||||
line-ending = "cr-lf"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't change quotes")
|
||||
|
||||
|
||||
if condition:
|
||||
|
||||
print("Should change quotes")
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
def foo(arg1, arg2):
|
||||
print("Shouldn't change quotes")
|
||||
|
||||
|
||||
if condition:
|
||||
print('Should change quotes')
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_option_inheritance() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
let base_toml = tempdir.path().join("base.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
extend = "base.toml"
|
||||
|
||||
[format]
|
||||
quote-style = "single"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
fs::write(
|
||||
base_toml,
|
||||
r#"
|
||||
[format]
|
||||
indent-style = "tab"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't change quotes")
|
||||
|
||||
|
||||
if condition:
|
||||
|
||||
print("Should change quotes")
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
def foo(
|
||||
arg1,
|
||||
arg2,
|
||||
):
|
||||
print("Shouldn't change quotes")
|
||||
|
||||
|
||||
if condition:
|
||||
print('Should change quotes')
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that the legacy `format` option continues to work but emits a warning.
|
||||
#[test]
|
||||
fn legacy_format_option() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
format = "json"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["check", "--select", "F401", "--no-cache", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
import os
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
[
|
||||
{
|
||||
"code": "F401",
|
||||
"end_location": {
|
||||
"column": 10,
|
||||
"row": 2
|
||||
},
|
||||
"filename": "-",
|
||||
"fix": {
|
||||
"applicability": "Automatic",
|
||||
"edits": [
|
||||
{
|
||||
"content": "",
|
||||
"end_location": {
|
||||
"column": 1,
|
||||
"row": 3
|
||||
},
|
||||
"location": {
|
||||
"column": 1,
|
||||
"row": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"message": "Remove unused import: `os`"
|
||||
},
|
||||
"location": {
|
||||
"column": 8,
|
||||
"row": 2
|
||||
},
|
||||
"message": "`os` imported but unused",
|
||||
"noqa_row": 2,
|
||||
"url": "https://docs.astral.sh/ruff/rules/unused-import"
|
||||
}
|
||||
]
|
||||
----- stderr -----
|
||||
warning: The option `format` has been deprecated to avoid ambiguity with Ruff's upcoming formatter. Use `output-format` instead.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
@@ -28,7 +28,7 @@ ruff_workspace = { path = "../ruff_workspace", features = ["schemars"]}
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
ignore = { workspace = true }
|
||||
indicatif = "0.17.5"
|
||||
indicatif = "0.17.7"
|
||||
itertools = { workspace = true }
|
||||
libcst = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
|
||||
@@ -34,7 +34,7 @@ use ruff_formatter::{FormatError, LineWidth, PrintError};
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::settings::types::{FilePattern, FilePatternSet};
|
||||
use ruff_python_formatter::{
|
||||
format_module, FormatModuleError, MagicTrailingComma, PyFormatOptions,
|
||||
format_module_source, FormatModuleError, MagicTrailingComma, PyFormatOptions,
|
||||
};
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, Resolver};
|
||||
|
||||
@@ -549,7 +549,6 @@ fn format_dir_entry(
|
||||
|
||||
let settings = resolver.resolve(&path, pyproject_config);
|
||||
// That's a bad way of doing this but it's not worth doing something better for format_dev
|
||||
// TODO(micha) use formatter settings instead
|
||||
if settings.formatter.line_width != LineWidth::default() {
|
||||
options = options.with_line_width(settings.formatter.line_width);
|
||||
}
|
||||
@@ -800,7 +799,7 @@ fn format_dev_file(
|
||||
let content = fs::read_to_string(input_path)?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
let start = Instant::now();
|
||||
let printed = match format_module(&content, options.clone()) {
|
||||
let printed = match format_module_source(&content, options.clone()) {
|
||||
Ok(printed) => printed,
|
||||
Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => {
|
||||
return Err(CheckFileError::SyntaxErrorInInput(err));
|
||||
@@ -827,7 +826,7 @@ fn format_dev_file(
|
||||
}
|
||||
|
||||
if stability_check {
|
||||
let reformatted = match format_module(formatted, options) {
|
||||
let reformatted = match format_module_source(formatted, options) {
|
||||
Ok(reformatted) => reformatted,
|
||||
Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => {
|
||||
return Err(CheckFileError::SyntaxErrorInOutput {
|
||||
|
||||
@@ -11,6 +11,7 @@ use strum::IntoEnumIterator;
|
||||
use ruff_diagnostics::AutofixKind;
|
||||
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::OptionsMetadata;
|
||||
|
||||
use crate::ROOT_DIR;
|
||||
|
||||
@@ -96,10 +97,7 @@ fn process_documentation(documentation: &str, out: &mut String) {
|
||||
if let Some(rest) = line.strip_prefix("- `") {
|
||||
let option = rest.trim_end().trim_end_matches('`');
|
||||
|
||||
assert!(
|
||||
Options::metadata().get(option).is_some(),
|
||||
"unknown option {option}"
|
||||
);
|
||||
assert!(Options::metadata().has(option), "unknown option {option}");
|
||||
|
||||
let anchor = option.replace('.', "-");
|
||||
out.push_str(&format!("- [`{option}`][{option}]\n"));
|
||||
|
||||
@@ -1,9 +1,74 @@
|
||||
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
|
||||
//!
|
||||
//! Used for <https://docs.astral.sh/ruff/settings/>.
|
||||
use itertools::Itertools;
|
||||
use std::fmt::Write;
|
||||
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::{OptionEntry, OptionField};
|
||||
use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
|
||||
pub(crate) fn generate() -> String {
|
||||
let mut output = String::new();
|
||||
generate_set(&mut output, &Set::Toplevel(Options::metadata()));
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn generate_set(output: &mut String, set: &Set) {
|
||||
writeln!(output, "### {title}\n", title = set.title()).unwrap();
|
||||
|
||||
if let Some(documentation) = set.metadata().documentation() {
|
||||
output.push_str(documentation);
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
let mut visitor = CollectOptionsVisitor::default();
|
||||
set.metadata().record(&mut visitor);
|
||||
|
||||
let (mut fields, mut sets) = (visitor.fields, visitor.groups);
|
||||
|
||||
fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
|
||||
sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
|
||||
|
||||
// Generate the fields.
|
||||
for (name, field) in &fields {
|
||||
emit_field(output, name, field, set.name());
|
||||
output.push_str("---\n\n");
|
||||
}
|
||||
|
||||
// Generate all the sub-sets.
|
||||
for (set_name, sub_set) in &sets {
|
||||
generate_set(output, &Set::Named(set_name, *sub_set));
|
||||
}
|
||||
}
|
||||
|
||||
enum Set<'a> {
|
||||
Toplevel(OptionSet),
|
||||
Named(&'a str, OptionSet),
|
||||
}
|
||||
|
||||
impl<'a> Set<'a> {
|
||||
fn name(&self) -> Option<&'a str> {
|
||||
match self {
|
||||
Set::Toplevel(_) => None,
|
||||
Set::Named(name, _) => Some(name),
|
||||
}
|
||||
}
|
||||
|
||||
fn title(&self) -> &'a str {
|
||||
match self {
|
||||
Set::Toplevel(_) => "Top-level",
|
||||
Set::Named(name, _) => name,
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata(&self) -> &OptionSet {
|
||||
match self {
|
||||
Set::Toplevel(set) => set,
|
||||
Set::Named(_, set) => set,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name: Option<&str>) {
|
||||
// if there's a group name, we need to add it to the anchor
|
||||
@@ -37,38 +102,18 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name:
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
pub(crate) fn generate() -> String {
|
||||
let mut output: String = "### Top-level\n\n".into();
|
||||
|
||||
let sorted_options: Vec<_> = Options::metadata()
|
||||
.into_iter()
|
||||
.sorted_by_key(|(name, _)| *name)
|
||||
.collect();
|
||||
|
||||
// Generate all the top-level fields.
|
||||
for (name, entry) in &sorted_options {
|
||||
let OptionEntry::Field(field) = entry else {
|
||||
continue;
|
||||
};
|
||||
emit_field(&mut output, name, field, None);
|
||||
output.push_str("---\n\n");
|
||||
}
|
||||
|
||||
// Generate all the sub-groups.
|
||||
for (group_name, entry) in &sorted_options {
|
||||
let OptionEntry::Group(fields) = entry else {
|
||||
continue;
|
||||
};
|
||||
output.push_str(&format!("### {group_name}\n"));
|
||||
output.push('\n');
|
||||
for (name, entry) in fields.iter().sorted_by_key(|(name, _)| name) {
|
||||
let OptionEntry::Field(field) = entry else {
|
||||
continue;
|
||||
};
|
||||
emit_field(&mut output, name, field, Some(group_name));
|
||||
output.push_str("---\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
#[derive(Default)]
|
||||
struct CollectOptionsVisitor {
|
||||
groups: Vec<(String, OptionSet)>,
|
||||
fields: Vec<(String, OptionField)>,
|
||||
}
|
||||
|
||||
impl Visit for CollectOptionsVisitor {
|
||||
fn record_set(&mut self, name: &str, group: OptionSet) {
|
||||
self.groups.push((name.to_owned(), group));
|
||||
}
|
||||
|
||||
fn record_field(&mut self, name: &str, field: OptionField) {
|
||||
self.fields.push((name.to_owned(), field));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use ruff_diagnostics::AutofixKind;
|
||||
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
|
||||
use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix;
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::OptionsMetadata;
|
||||
|
||||
const FIX_SYMBOL: &str = "🛠️";
|
||||
const PREVIEW_SYMBOL: &str = "🧪";
|
||||
@@ -104,10 +105,7 @@ pub(crate) fn generate() -> String {
|
||||
table_out.push('\n');
|
||||
}
|
||||
|
||||
if Options::metadata()
|
||||
.iter()
|
||||
.any(|(name, _)| name == &linter.name())
|
||||
{
|
||||
if Options::metadata().has(linter.name()) {
|
||||
table_out.push_str(&format!(
|
||||
"For related settings, see [{}](settings.md#{}).",
|
||||
linter.name(),
|
||||
|
||||
@@ -55,7 +55,11 @@ use ruff_macros::CacheKey;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, CacheKey)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(rename_all = "kebab-case")
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Default)]
|
||||
pub enum IndentStyle {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
|
||||
use ruff_macros::CacheKey;
|
||||
|
||||
/// Options that affect how the [`crate::Printer`] prints the format tokens
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default)]
|
||||
@@ -121,7 +120,7 @@ impl SourceMapGeneration {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum LineEnding {
|
||||
/// Line Feed only (\n), common on Linux and macOS as well as inside git repos
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.0.290"
|
||||
version = "0.0.291"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -45,8 +45,6 @@ libcst = { workspace = true }
|
||||
log = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
natord = { version = "1.0.9" }
|
||||
num-bigint = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
path-absolutize = { workspace = true, features = [
|
||||
"once_cell_cache",
|
||||
@@ -60,7 +58,7 @@ regex = { workspace = true }
|
||||
result-like = { version = "0.4.6" }
|
||||
rustc-hash = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
semver = { version = "1.0.16" }
|
||||
semver = { version = "1.0.19" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
similar = { workspace = true }
|
||||
|
||||
@@ -20,3 +20,4 @@ os.chmod(keyfile, stat.S_IRWXO | stat.S_IRWXG | stat.S_IRWXU) # Error
|
||||
os.chmod("~/hidden_exec", stat.S_IXGRP) # Error
|
||||
os.chmod("~/hidden_exec", stat.S_IXOTH) # OK
|
||||
os.chmod("/etc/passwd", stat.S_IWOTH) # Error
|
||||
os.chmod("/etc/passwd", 0o100000000) # Error
|
||||
|
||||
@@ -56,3 +56,7 @@ setattr(foo.bar, r"baz", None)
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
|
||||
assert getattr(func, '_rpc')is True
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1732387247
|
||||
getattr(*foo, "bar")
|
||||
setattr(*foo, "bar", None)
|
||||
|
||||
@@ -15,3 +15,29 @@ def ok_complex_logic():
|
||||
def error():
|
||||
resource = acquire_resource()
|
||||
yield resource
|
||||
|
||||
|
||||
import typing
|
||||
from typing import Generator
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def ok_complex_logic() -> typing.Generator[Resource, None, None]:
|
||||
if some_condition:
|
||||
resource = acquire_resource()
|
||||
yield resource
|
||||
resource.release()
|
||||
return
|
||||
yield None
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def error() -> typing.Generator[typing.Any, None, None]:
|
||||
resource = acquire_resource()
|
||||
yield resource
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def error() -> Generator[Resource, None, None]:
|
||||
resource = acquire_resource()
|
||||
yield resource
|
||||
|
||||
4
crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM105_4.py
vendored
Executable file
4
crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM105_4.py
vendored
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
try:
|
||||
from __builtin__ import bytes, str, open, super, range, zip, round, int, pow, object, input
|
||||
except ImportError: pass
|
||||
4
crates/ruff_linter/resources/test/fixtures/pydocstyle/D215.py
vendored
Normal file
4
crates/ruff_linter/resources/test/fixtures/pydocstyle/D215.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
TODO:
|
||||
-
|
||||
"""
|
||||
@@ -104,3 +104,12 @@ def get_owner_id_from_mac_address():
|
||||
mac_address = get_primary_mac_address()
|
||||
except(IOError, OSError) as ex:
|
||||
msg = 'Unable to query URL to get Owner ID: {u}\n{e}'.format(u=owner_id_url, e=ex)
|
||||
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7580
|
||||
import os
|
||||
|
||||
try:
|
||||
pass
|
||||
except os.error:
|
||||
pass
|
||||
|
||||
@@ -184,3 +184,15 @@ if sys.version_info < (3,12):
|
||||
|
||||
if sys.version_info <= (3,12):
|
||||
print("py3")
|
||||
|
||||
if sys.version_info <= (3,12):
|
||||
print("py3")
|
||||
|
||||
if sys.version_info == 10000000:
|
||||
print("py3")
|
||||
|
||||
if sys.version_info < (3,10000000):
|
||||
print("py3")
|
||||
|
||||
if sys.version_info <= (3,10000000):
|
||||
print("py3")
|
||||
|
||||
33
crates/ruff_linter/resources/test/fixtures/refurb/FURB105.py
vendored
Normal file
33
crates/ruff_linter/resources/test/fixtures/refurb/FURB105.py
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# Errors.
|
||||
|
||||
print("")
|
||||
print("", sep=",")
|
||||
print("", end="bar")
|
||||
print("", sep=",", end="bar")
|
||||
print(sep="")
|
||||
print("", sep="")
|
||||
print("", "", sep="")
|
||||
print("", "", sep="", end="")
|
||||
print("", "", sep="", end="bar")
|
||||
print("", sep="", end="bar")
|
||||
print(sep="", end="bar")
|
||||
print("", "foo", sep="")
|
||||
print("foo", "", sep="")
|
||||
print("foo", "", "bar", sep="")
|
||||
print("", *args)
|
||||
print("", *args, sep="")
|
||||
print("", **kwargs)
|
||||
print(sep="\t")
|
||||
|
||||
# OK.
|
||||
|
||||
print()
|
||||
print("foo")
|
||||
print("", "")
|
||||
print("", "foo")
|
||||
print("foo", "")
|
||||
print("", "", sep=",")
|
||||
print("", "foo", sep=",")
|
||||
print("foo", "", sep=",")
|
||||
print("foo", "", "bar", "", sep=",")
|
||||
print("", "", **kwargs)
|
||||
@@ -895,6 +895,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnsupportedMethodCallOnAll) {
|
||||
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);
|
||||
}
|
||||
if checker.enabled(Rule::PrintEmptyString) {
|
||||
refurb::rules::print_empty_string(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::QuadraticListSummation) {
|
||||
ruff::rules::quadratic_list_summation(checker, call);
|
||||
}
|
||||
|
||||
@@ -292,6 +292,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
stmt,
|
||||
name,
|
||||
parameters,
|
||||
returns.as_deref(),
|
||||
decorator_list,
|
||||
body,
|
||||
);
|
||||
@@ -695,7 +696,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
},
|
||||
) => {
|
||||
let module = module.as_deref();
|
||||
let level = level.map(|level| level.to_u32());
|
||||
let level = *level;
|
||||
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
|
||||
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
|
||||
}
|
||||
|
||||
@@ -358,7 +358,7 @@ where
|
||||
range: _,
|
||||
}) => {
|
||||
let module = module.as_deref();
|
||||
let level = level.map(|level| level.to_u32());
|
||||
let level = *level;
|
||||
for alias in names {
|
||||
if let Some("__future__") = module {
|
||||
let name = alias.asname.as_ref().unwrap_or(&alias.name);
|
||||
|
||||
@@ -44,7 +44,7 @@ fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) ->
|
||||
level,
|
||||
range: _,
|
||||
}) => {
|
||||
let level = level.map_or(0, |level| level.to_usize());
|
||||
let level = level.unwrap_or_default() as usize;
|
||||
let module = if let Some(module) = module {
|
||||
let module: &String = module.as_ref();
|
||||
if level == 0 {
|
||||
@@ -95,6 +95,7 @@ pub(crate) fn check_imports(
|
||||
tracker.visit_body(python_ast);
|
||||
tracker
|
||||
};
|
||||
|
||||
let blocks: Vec<&Block> = tracker.iter().collect();
|
||||
|
||||
// Enforce import rules.
|
||||
|
||||
@@ -914,6 +914,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Slots, "002") => (RuleGroup::Unspecified, rules::flake8_slots::rules::NoSlotsInNamedtupleSubclass),
|
||||
|
||||
// refurb
|
||||
(Refurb, "105") => (RuleGroup::Preview, rules::refurb::rules::PrintEmptyString),
|
||||
#[allow(deprecated)]
|
||||
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
|
||||
#[allow(deprecated)]
|
||||
|
||||
@@ -64,7 +64,7 @@ impl<'a> Insertion<'a> {
|
||||
// Otherwise, advance to the next row.
|
||||
locator.full_line_end(location)
|
||||
} else {
|
||||
TextSize::default()
|
||||
locator.contents_start()
|
||||
};
|
||||
|
||||
// Skip over commented lines, with whitespace separation.
|
||||
|
||||
@@ -308,7 +308,7 @@ impl<'a> Importer<'a> {
|
||||
range: _,
|
||||
}) = stmt
|
||||
{
|
||||
if level.map_or(true, |level| level.to_u32() == 0)
|
||||
if level.map_or(true, |level| level == 0)
|
||||
&& name.as_ref().is_some_and(|name| name == module)
|
||||
&& names.iter().all(|alias| alias.name.as_str() != "*")
|
||||
{
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use num_bigint::BigInt;
|
||||
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -237,7 +235,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], compara
|
||||
..
|
||||
}) = slice.as_ref()
|
||||
{
|
||||
if *i == BigInt::from(0) {
|
||||
if *i == 0 {
|
||||
if let (
|
||||
[CmpOp::Eq | CmpOp::NotEq],
|
||||
[Expr::Constant(ast::ExprConstant {
|
||||
@@ -246,13 +244,13 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], compara
|
||||
})],
|
||||
) = (ops, comparators)
|
||||
{
|
||||
if *n == BigInt::from(3) && checker.enabled(Rule::SysVersionInfo0Eq3) {
|
||||
if *n == 3 && checker.enabled(Rule::SysVersionInfo0Eq3) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersionInfo0Eq3, left.range()));
|
||||
}
|
||||
}
|
||||
} else if *i == BigInt::from(1) {
|
||||
} else if *i == 1 {
|
||||
if let (
|
||||
[CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE],
|
||||
[Expr::Constant(ast::ExprConstant {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use num_bigint::BigInt;
|
||||
use ruff_python_ast::{self as ast, Constant, Expr};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Constant, Expr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -184,11 +182,11 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
|
||||
..
|
||||
}) = upper.as_ref()
|
||||
{
|
||||
if *i == BigInt::from(1) && checker.enabled(Rule::SysVersionSlice1) {
|
||||
if *i == 1 && checker.enabled(Rule::SysVersionSlice1) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersionSlice1, value.range()));
|
||||
} else if *i == BigInt::from(3) && checker.enabled(Rule::SysVersionSlice3) {
|
||||
} else if *i == 3 && checker.enabled(Rule::SysVersionSlice3) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersionSlice3, value.range()));
|
||||
@@ -200,11 +198,11 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
|
||||
value: Constant::Int(i),
|
||||
..
|
||||
}) => {
|
||||
if *i == BigInt::from(2) && checker.enabled(Rule::SysVersion2) {
|
||||
if *i == 2 && checker.enabled(Rule::SysVersion2) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersion2, value.range()));
|
||||
} else if *i == BigInt::from(0) && checker.enabled(Rule::SysVersion0) {
|
||||
} else if *i == 0 && checker.enabled(Rule::SysVersion0) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SysVersion0, value.range()));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use num_traits::ToPrimitive;
|
||||
use anyhow::Result;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
@@ -36,17 +36,28 @@ use crate::checkers::ast::Checker;
|
||||
/// - [Common Weakness Enumeration: CWE-732](https://cwe.mitre.org/data/definitions/732.html)
|
||||
#[violation]
|
||||
pub struct BadFilePermissions {
|
||||
mask: u16,
|
||||
reason: Reason,
|
||||
}
|
||||
|
||||
impl Violation for BadFilePermissions {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BadFilePermissions { mask } = self;
|
||||
format!("`os.chmod` setting a permissive mask `{mask:#o}` on file or directory")
|
||||
let BadFilePermissions { reason } = self;
|
||||
match reason {
|
||||
Reason::Permissive(mask) => {
|
||||
format!("`os.chmod` setting a permissive mask `{mask:#o}` on file or directory")
|
||||
}
|
||||
Reason::Invalid => format!("`os.chmod` setting an invalid mask on file or directory"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Reason {
|
||||
Permissive(u16),
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// S103
|
||||
pub(crate) fn bad_file_permissions(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if checker
|
||||
@@ -55,10 +66,26 @@ pub(crate) fn bad_file_permissions(checker: &mut Checker, call: &ast::ExprCall)
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["os", "chmod"]))
|
||||
{
|
||||
if let Some(mode_arg) = call.arguments.find_argument("mode", 1) {
|
||||
if let Some(int_value) = int_value(mode_arg, checker.semantic()) {
|
||||
if (int_value & WRITE_WORLD > 0) || (int_value & EXECUTE_GROUP > 0) {
|
||||
match parse_mask(mode_arg, checker.semantic()) {
|
||||
// The mask couldn't be determined (e.g., it's dynamic).
|
||||
Ok(None) => {}
|
||||
// The mask is a valid integer value -- check for overly permissive permissions.
|
||||
Ok(Some(mask)) => {
|
||||
if (mask & WRITE_WORLD > 0) || (mask & EXECUTE_GROUP > 0) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BadFilePermissions {
|
||||
reason: Reason::Permissive(mask),
|
||||
},
|
||||
mode_arg.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
// The mask is an invalid integer value (i.e., it's out of range).
|
||||
Err(_) => {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BadFilePermissions { mask: int_value },
|
||||
BadFilePermissions {
|
||||
reason: Reason::Invalid,
|
||||
},
|
||||
mode_arg.range(),
|
||||
));
|
||||
}
|
||||
@@ -113,28 +140,37 @@ fn py_stat(call_path: &CallPath) -> Option<u16> {
|
||||
}
|
||||
}
|
||||
|
||||
fn int_value(expr: &Expr, semantic: &SemanticModel) -> Option<u16> {
|
||||
/// Return the mask value as a `u16`, if it can be determined. Returns an error if the mask is
|
||||
/// an integer value, but that value is out of range.
|
||||
fn parse_mask(expr: &Expr, semantic: &SemanticModel) -> Result<Option<u16>> {
|
||||
match expr {
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(value),
|
||||
value: Constant::Int(int),
|
||||
..
|
||||
}) => value.to_u16(),
|
||||
Expr::Attribute(_) => semantic.resolve_call_path(expr).as_ref().and_then(py_stat),
|
||||
}) => match int.as_u16() {
|
||||
Some(value) => Ok(Some(value)),
|
||||
None => anyhow::bail!("int value out of range"),
|
||||
},
|
||||
Expr::Attribute(_) => Ok(semantic.resolve_call_path(expr).as_ref().and_then(py_stat)),
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
op,
|
||||
right,
|
||||
range: _,
|
||||
}) => {
|
||||
let left_value = int_value(left, semantic)?;
|
||||
let right_value = int_value(right, semantic)?;
|
||||
match op {
|
||||
let Some(left_value) = parse_mask(left, semantic)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(right_value) = parse_mask(right, semantic)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(match op {
|
||||
Operator::BitAnd => Some(left_value & right_value),
|
||||
Operator::BitOr => Some(left_value | right_value),
|
||||
Operator::BitXor => Some(left_value ^ right_value),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use num_traits::{One, Zero};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Constant, Expr};
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, Int};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -52,16 +50,16 @@ pub(crate) fn snmp_insecure_version(checker: &mut Checker, call: &ast::ExprCall)
|
||||
})
|
||||
{
|
||||
if let Some(keyword) = call.arguments.find_keyword("mpModel") {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(value),
|
||||
..
|
||||
}) = &keyword.value
|
||||
{
|
||||
if value.is_zero() || value.is_one() {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SnmpInsecureVersion, keyword.range()));
|
||||
}
|
||||
if matches!(
|
||||
keyword.value,
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(Int::ZERO | Int::ONE),
|
||||
..
|
||||
})
|
||||
) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SnmpInsecureVersion, keyword.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,15 @@ S103.py:22:25: S103 `os.chmod` setting a permissive mask `0o2` on file or direct
|
||||
21 | os.chmod("~/hidden_exec", stat.S_IXOTH) # OK
|
||||
22 | os.chmod("/etc/passwd", stat.S_IWOTH) # Error
|
||||
| ^^^^^^^^^^^^ S103
|
||||
23 | os.chmod("/etc/passwd", 0o100000000) # Error
|
||||
|
|
||||
|
||||
S103.py:23:25: S103 `os.chmod` setting an invalid mask on file or directory
|
||||
|
|
||||
21 | os.chmod("~/hidden_exec", stat.S_IXOTH) # OK
|
||||
22 | os.chmod("/etc/passwd", stat.S_IWOTH) # Error
|
||||
23 | os.chmod("/etc/passwd", 0o100000000) # Error
|
||||
| ^^^^^^^^^^^ S103
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -64,6 +64,9 @@ pub(crate) fn getattr_with_constant(
|
||||
let [obj, arg] = args else {
|
||||
return;
|
||||
};
|
||||
if obj.is_starred_expr() {
|
||||
return;
|
||||
}
|
||||
let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(ast::StringConstant { value, .. }),
|
||||
..
|
||||
@@ -77,6 +80,9 @@ pub(crate) fn getattr_with_constant(
|
||||
if is_mangled_private(value) {
|
||||
return;
|
||||
}
|
||||
if !checker.semantic().is_builtin("getattr") {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(GetAttrWithConstant, expr.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
|
||||
@@ -78,6 +78,9 @@ pub(crate) fn setattr_with_constant(
|
||||
let [obj, name, value] = args else {
|
||||
return;
|
||||
};
|
||||
if obj.is_starred_expr() {
|
||||
return;
|
||||
}
|
||||
let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(name),
|
||||
..
|
||||
@@ -91,6 +94,10 @@ pub(crate) fn setattr_with_constant(
|
||||
if is_mangled_private(name) {
|
||||
return;
|
||||
}
|
||||
if !checker.semantic().is_builtin("setattr") {
|
||||
return;
|
||||
}
|
||||
|
||||
// We can only replace a `setattr` call (which is an `Expr`) with an assignment
|
||||
// (which is a `Stmt`) if the `Expr` is already being used as a `Stmt`
|
||||
// (i.e., it's directly within an `Stmt::Expr`).
|
||||
|
||||
@@ -21,14 +21,25 @@ use crate::checkers::ast::Checker;
|
||||
/// `str.removesuffix` to remove an exact prefix or suffix from a string,
|
||||
/// respectively, which should be preferred when possible.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// As a heuristic, this rule only flags multi-character strings that contain
|
||||
/// duplicate characters. This allows usages like `.strip("xyz")`, which
|
||||
/// removes all occurrences of the characters `x`, `y`, and `z` from the
|
||||
/// leading and trailing ends of the string, but not `.strip("foo")`.
|
||||
///
|
||||
/// The use of unique, multi-character strings may be intentional and
|
||||
/// consistent with the intent of `.strip()`, `.lstrip()`, or `.rstrip()`,
|
||||
/// while the use of duplicate-character strings is very likely to be a
|
||||
/// mistake.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// "abcba".strip("ab") # "c"
|
||||
/// "text.txt".strip(".txt") # "ex"
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// "abcba".removeprefix("ab").removesuffix("ba") # "c"
|
||||
/// "text.txt".removesuffix(".txt") # "text"
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
@@ -39,7 +50,7 @@ pub struct StripWithMultiCharacters;
|
||||
impl Violation for StripWithMultiCharacters {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Using `.strip()` with multi-character strings is misleading the reader")
|
||||
format!("Using `.strip()` with multi-character strings is misleading")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +76,7 @@ pub(crate) fn strip_with_multi_characters(
|
||||
return;
|
||||
};
|
||||
|
||||
let num_chars = value.chars().count();
|
||||
if num_chars > 1 && num_chars != value.chars().unique().count() {
|
||||
if value.chars().count() > 1 && !value.chars().all_unique() {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(StripWithMultiCharacters, expr.range()));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B005.py:4:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
||||
B005.py:4:1: B005 Using `.strip()` with multi-character strings is misleading
|
||||
|
|
||||
2 | s.strip(s) # no warning
|
||||
3 | s.strip("we") # no warning
|
||||
@@ -11,7 +11,7 @@ B005.py:4:1: B005 Using `.strip()` with multi-character strings is misleading th
|
||||
6 | s.strip("\n\t ") # no warning
|
||||
|
|
||||
|
||||
B005.py:7:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
||||
B005.py:7:1: B005 Using `.strip()` with multi-character strings is misleading
|
||||
|
|
||||
5 | s.strip("e") # no warning
|
||||
6 | s.strip("\n\t ") # no warning
|
||||
@@ -21,7 +21,7 @@ B005.py:7:1: B005 Using `.strip()` with multi-character strings is misleading th
|
||||
9 | s.lstrip("we") # no warning
|
||||
|
|
||||
|
||||
B005.py:10:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
||||
B005.py:10:1: B005 Using `.strip()` with multi-character strings is misleading
|
||||
|
|
||||
8 | s.lstrip(s) # no warning
|
||||
9 | s.lstrip("we") # no warning
|
||||
@@ -31,7 +31,7 @@ B005.py:10:1: B005 Using `.strip()` with multi-character strings is misleading t
|
||||
12 | s.lstrip("\n\t ") # no warning
|
||||
|
|
||||
|
||||
B005.py:13:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
||||
B005.py:13:1: B005 Using `.strip()` with multi-character strings is misleading
|
||||
|
|
||||
11 | s.lstrip("e") # no warning
|
||||
12 | s.lstrip("\n\t ") # no warning
|
||||
@@ -41,7 +41,7 @@ B005.py:13:1: B005 Using `.strip()` with multi-character strings is misleading t
|
||||
15 | s.rstrip("we") # warning
|
||||
|
|
||||
|
||||
B005.py:16:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
||||
B005.py:16:1: B005 Using `.strip()` with multi-character strings is misleading
|
||||
|
|
||||
14 | s.rstrip(s) # no warning
|
||||
15 | s.rstrip("we") # warning
|
||||
@@ -51,7 +51,7 @@ B005.py:16:1: B005 Using `.strip()` with multi-character strings is misleading t
|
||||
18 | s.rstrip("\n\t ") # no warning
|
||||
|
|
||||
|
||||
B005.py:19:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
||||
B005.py:19:1: B005 Using `.strip()` with multi-character strings is misleading
|
||||
|
|
||||
17 | s.rstrip("e") # no warning
|
||||
18 | s.rstrip("\n\t ") # no warning
|
||||
@@ -61,7 +61,7 @@ B005.py:19:1: B005 Using `.strip()` with multi-character strings is misleading t
|
||||
21 | s.strip("あ") # no warning
|
||||
|
|
||||
|
||||
B005.py:22:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
||||
B005.py:22:1: B005 Using `.strip()` with multi-character strings is misleading
|
||||
|
|
||||
20 | s.strip("a") # no warning
|
||||
21 | s.strip("あ") # no warning
|
||||
@@ -71,7 +71,7 @@ B005.py:22:1: B005 Using `.strip()` with multi-character strings is misleading t
|
||||
24 | s.strip("\u0074\u0065\u0073\u0074") # warning
|
||||
|
|
||||
|
||||
B005.py:24:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
||||
B005.py:24:1: B005 Using `.strip()` with multi-character strings is misleading
|
||||
|
|
||||
22 | s.strip("ああ") # warning
|
||||
23 | s.strip("\ufeff") # no warning
|
||||
|
||||
@@ -321,6 +321,8 @@ B009_B010.py:58:8: B009 [*] Do not call `getattr` with a constant attribute valu
|
||||
57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
|
||||
58 | assert getattr(func, '_rpc')is True
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ B009
|
||||
59 |
|
||||
60 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1732387247
|
||||
|
|
||||
= help: Replace `getattr` with attribute access
|
||||
|
||||
@@ -330,5 +332,8 @@ B009_B010.py:58:8: B009 [*] Do not call `getattr` with a constant attribute valu
|
||||
57 57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
|
||||
58 |-assert getattr(func, '_rpc')is True
|
||||
58 |+assert func._rpc is True
|
||||
59 59 |
|
||||
60 60 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1732387247
|
||||
61 61 | getattr(*foo, "bar")
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use num_bigint::BigInt;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, UnaryOp};
|
||||
@@ -93,7 +91,7 @@ pub(crate) fn unnecessary_subscript_reversal(
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if *val != BigInt::from(1) {
|
||||
if *val != 1 {
|
||||
return;
|
||||
};
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use num_bigint::BigInt;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
@@ -75,7 +73,7 @@ pub(crate) fn unnecessary_range_start(checker: &mut Checker, call: &ast::ExprCal
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if *value != BigInt::from(0) {
|
||||
if *value != 0 {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use num_bigint::BigInt;
|
||||
use num_traits::{One, Zero};
|
||||
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::map_subscript;
|
||||
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr, Int};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -249,18 +246,18 @@ impl ExpectedComparator {
|
||||
..
|
||||
}) = upper.as_ref()
|
||||
{
|
||||
if *upper == BigInt::one() {
|
||||
if *upper == 1 {
|
||||
return Some(ExpectedComparator::MajorTuple);
|
||||
}
|
||||
if *upper == BigInt::from(2) {
|
||||
if *upper == 2 {
|
||||
return Some(ExpectedComparator::MajorMinorTuple);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(n),
|
||||
value: Constant::Int(Int::ZERO),
|
||||
..
|
||||
}) if n.is_zero() => {
|
||||
}) => {
|
||||
return Some(ExpectedComparator::MajorDigit);
|
||||
}
|
||||
_ => (),
|
||||
|
||||
@@ -764,7 +764,13 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D
|
||||
}
|
||||
|
||||
/// PT004, PT005, PT022
|
||||
fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: &[Stmt]) {
|
||||
fn check_fixture_returns(
|
||||
checker: &mut Checker,
|
||||
stmt: &Stmt,
|
||||
name: &str,
|
||||
body: &[Stmt],
|
||||
returns: Option<&Expr>,
|
||||
) {
|
||||
let mut visitor = SkipFunctionsVisitor::default();
|
||||
|
||||
for stmt in body {
|
||||
@@ -795,27 +801,50 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: &
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::PytestUselessYieldFixture) {
|
||||
if let Some(stmt) = body.last() {
|
||||
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt {
|
||||
if value.is_yield_expr() {
|
||||
if visitor.yield_statements.len() == 1 {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
PytestUselessYieldFixture {
|
||||
name: name.to_string(),
|
||||
},
|
||||
stmt.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
"return".to_string(),
|
||||
TextRange::at(stmt.start(), "yield".text_len()),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
let Some(stmt) = body.last() else {
|
||||
return;
|
||||
};
|
||||
let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else {
|
||||
return;
|
||||
};
|
||||
if !value.is_yield_expr() {
|
||||
return;
|
||||
}
|
||||
if visitor.yield_statements.len() != 1 {
|
||||
return;
|
||||
}
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
PytestUselessYieldFixture {
|
||||
name: name.to_string(),
|
||||
},
|
||||
stmt.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
let yield_edit = Edit::range_replacement(
|
||||
"return".to_string(),
|
||||
TextRange::at(stmt.start(), "yield".text_len()),
|
||||
);
|
||||
let return_type_edit = returns.and_then(|returns| {
|
||||
let ast::ExprSubscript { value, slice, .. } = returns.as_subscript_expr()?;
|
||||
let ast::ExprTuple { elts, .. } = slice.as_tuple_expr()?;
|
||||
let [first, ..] = elts.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
if !checker.semantic().match_typing_expr(value, "Generator") {
|
||||
return None;
|
||||
}
|
||||
Some(Edit::range_replacement(
|
||||
checker.generator().expr(first),
|
||||
returns.range(),
|
||||
))
|
||||
});
|
||||
if let Some(return_type_edit) = return_type_edit {
|
||||
diagnostic.set_fix(Fix::automatic_edits(yield_edit, [return_type_edit]));
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::automatic(yield_edit));
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -910,6 +939,7 @@ pub(crate) fn fixture(
|
||||
stmt: &Stmt,
|
||||
name: &str,
|
||||
parameters: &Parameters,
|
||||
returns: Option<&Expr>,
|
||||
decorators: &[Decorator],
|
||||
body: &[Stmt],
|
||||
) {
|
||||
@@ -933,7 +963,7 @@ pub(crate) fn fixture(
|
||||
|| checker.enabled(Rule::PytestUselessYieldFixture))
|
||||
&& !is_abstract(decorators, checker.semantic())
|
||||
{
|
||||
check_fixture_returns(checker, stmt, name, body);
|
||||
check_fixture_returns(checker, stmt, name, body, returns);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::PytestFixtureFinalizerCallback) {
|
||||
|
||||
@@ -16,5 +16,49 @@ PT022.py:17:5: PT022 [*] No teardown in fixture `error`, use `return` instead of
|
||||
16 16 | resource = acquire_resource()
|
||||
17 |- yield resource
|
||||
17 |+ return resource
|
||||
18 18 |
|
||||
19 19 |
|
||||
20 20 | import typing
|
||||
|
||||
PT022.py:37:5: PT022 [*] No teardown in fixture `error`, use `return` instead of `yield`
|
||||
|
|
||||
35 | def error() -> typing.Generator[typing.Any, None, None]:
|
||||
36 | resource = acquire_resource()
|
||||
37 | yield resource
|
||||
| ^^^^^^^^^^^^^^ PT022
|
||||
|
|
||||
= help: Replace `yield` with `return`
|
||||
|
||||
ℹ Fix
|
||||
32 32 |
|
||||
33 33 |
|
||||
34 34 | @pytest.fixture()
|
||||
35 |-def error() -> typing.Generator[typing.Any, None, None]:
|
||||
35 |+def error() -> typing.Any:
|
||||
36 36 | resource = acquire_resource()
|
||||
37 |- yield resource
|
||||
37 |+ return resource
|
||||
38 38 |
|
||||
39 39 |
|
||||
40 40 | @pytest.fixture()
|
||||
|
||||
PT022.py:43:5: PT022 [*] No teardown in fixture `error`, use `return` instead of `yield`
|
||||
|
|
||||
41 | def error() -> Generator[Resource, None, None]:
|
||||
42 | resource = acquire_resource()
|
||||
43 | yield resource
|
||||
| ^^^^^^^^^^^^^^ PT022
|
||||
|
|
||||
= help: Replace `yield` with `return`
|
||||
|
||||
ℹ Fix
|
||||
38 38 |
|
||||
39 39 |
|
||||
40 40 | @pytest.fixture()
|
||||
41 |-def error() -> Generator[Resource, None, None]:
|
||||
41 |+def error() -> Resource:
|
||||
42 42 | resource = acquire_resource()
|
||||
43 |- yield resource
|
||||
43 |+ return resource
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ mod tests {
|
||||
#[test_case(Rule::SuppressibleException, Path::new("SIM105_1.py"))]
|
||||
#[test_case(Rule::SuppressibleException, Path::new("SIM105_2.py"))]
|
||||
#[test_case(Rule::SuppressibleException, Path::new("SIM105_3.py"))]
|
||||
#[test_case(Rule::SuppressibleException, Path::new("SIM105_4.py"))]
|
||||
#[test_case(Rule::ReturnInTryExceptFinally, Path::new("SIM107.py"))]
|
||||
#[test_case(Rule::IfElseBlockInsteadOfIfExp, Path::new("SIM108.py"))]
|
||||
#[test_case(Rule::CompareWithTuple, Path::new("SIM109.py"))]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM105_4.py:2:1: SIM105 [*] Use `contextlib.suppress(ImportError)` instead of `try`-`except`-`pass`
|
||||
|
|
||||
1 | #!/usr/bin/env python
|
||||
2 | / try:
|
||||
3 | | from __builtin__ import bytes, str, open, super, range, zip, round, int, pow, object, input
|
||||
4 | | except ImportError: pass
|
||||
| |___________________________^ SIM105
|
||||
|
|
||||
= help: Replace with `contextlib.suppress(ImportError)`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | #!/usr/bin/env python
|
||||
2 |-try:
|
||||
2 |+import contextlib
|
||||
3 |+with contextlib.suppress(ImportError):
|
||||
3 4 | from __builtin__ import bytes, str, open, super, range, zip, round, int, pow, object, input
|
||||
4 |-except ImportError: pass
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_python_ast::{self as ast, Identifier, Int, Stmt};
|
||||
use ruff_python_ast::{self as ast, Identifier, Stmt};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
@@ -99,7 +99,7 @@ fn fix_banned_relative_import(
|
||||
TextRange::default(),
|
||||
)),
|
||||
names: names.clone(),
|
||||
level: Some(Int::new(0)),
|
||||
level: Some(0),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
let content = generator.stmt(&node.into());
|
||||
|
||||
@@ -118,7 +118,7 @@ pub(crate) fn annotate_imports<'a>(
|
||||
AnnotatedImport::ImportFrom {
|
||||
module: module.as_deref(),
|
||||
names: aliases,
|
||||
level: level.map(|level| level.to_u32()),
|
||||
level: *level,
|
||||
trailing_comma: if split_on_trailing_comma {
|
||||
trailing_comma(import, locator, source_type)
|
||||
} else {
|
||||
|
||||
@@ -75,7 +75,7 @@ fn includes_import(stmt: &Stmt, target: &AnyImport) -> bool {
|
||||
return false;
|
||||
};
|
||||
module.as_deref() == target.module
|
||||
&& level.map(|level| level.to_u32()) == target.level
|
||||
&& *level == target.level
|
||||
&& names.iter().any(|alias| {
|
||||
&alias.name == target.name.name
|
||||
&& alias.asname.as_deref() == target.name.as_name
|
||||
@@ -166,7 +166,7 @@ pub(crate) fn add_required_imports(
|
||||
name: name.name.as_str(),
|
||||
as_name: name.asname.as_deref(),
|
||||
},
|
||||
level: level.map(|level| level.to_u32()),
|
||||
level: *level,
|
||||
}),
|
||||
python_ast,
|
||||
locator,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use num_traits::One;
|
||||
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr, Int};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -80,7 +78,13 @@ pub(crate) fn nunique_constant_series_check(
|
||||
}
|
||||
|
||||
// Right should be the integer 1.
|
||||
if !is_constant_one(right) {
|
||||
if !matches!(
|
||||
right,
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(Int::ONE),
|
||||
range: _,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,14 +114,3 @@ pub(crate) fn nunique_constant_series_check(
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Return `true` if an [`Expr`] is a constant `1`.
|
||||
fn is_constant_one(expr: &Expr) -> bool {
|
||||
match expr {
|
||||
Expr::Constant(constant) => match &constant.value {
|
||||
Constant::Int(int) => int.is_one(),
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ mod tests {
|
||||
#[test_case(Rule::SectionNameEndsInColon, Path::new("D.py"))]
|
||||
#[test_case(Rule::SectionNotOverIndented, Path::new("sections.py"))]
|
||||
#[test_case(Rule::SectionNotOverIndented, Path::new("D214_module.py"))]
|
||||
#[test_case(Rule::SectionUnderlineNotOverIndented, Path::new("D215.py"))]
|
||||
#[test_case(Rule::SectionUnderlineAfterName, Path::new("sections.py"))]
|
||||
#[test_case(Rule::SectionUnderlineMatchesSectionLength, Path::new("sections.py"))]
|
||||
#[test_case(Rule::SectionUnderlineNotOverIndented, Path::new("sections.py"))]
|
||||
|
||||
@@ -1440,16 +1440,17 @@ fn blanks_and_section_underline(
|
||||
docstring.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
// Replace the existing indentation with whitespace of the appropriate length.
|
||||
let range = TextRange::at(
|
||||
blank_lines_end,
|
||||
leading_space.text_len() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace the existing indentation with whitespace of the appropriate length.
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
clean_space(docstring.indentation),
|
||||
range,
|
||||
)));
|
||||
let contents = clean_space(docstring.indentation);
|
||||
diagnostic.set_fix(Fix::automatic(if contents.is_empty() {
|
||||
Edit::range_deletion(range)
|
||||
} else {
|
||||
Edit::range_replacement(contents, range)
|
||||
}));
|
||||
};
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
|
||||
---
|
||||
D215.py:1:1: D215 [*] Section underline is over-indented ("TODO")
|
||||
|
|
||||
1 | / """
|
||||
2 | | TODO:
|
||||
3 | | -
|
||||
4 | | """
|
||||
| |___^ D215
|
||||
|
|
||||
= help: Remove over-indentation from "TODO" underline
|
||||
|
||||
ℹ Fix
|
||||
1 1 | """
|
||||
2 2 | TODO:
|
||||
3 |- -
|
||||
3 |+
|
||||
4 4 | """
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use itertools::Itertools;
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, UnaryOp};
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, Int, UnaryOp};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
@@ -83,7 +83,7 @@ fn is_magic_value(constant: &Constant, allowed_types: &[ConstantType]) -> bool {
|
||||
Constant::Str(ast::StringConstant { value, .. }) => {
|
||||
!matches!(value.as_str(), "" | "__main__")
|
||||
}
|
||||
Constant::Int(value) => !matches!(value.try_into(), Ok(0 | 1)),
|
||||
Constant::Int(value) => !matches!(*value, Int::ZERO | Int::ONE),
|
||||
Constant::Bytes(_) => true,
|
||||
Constant::Float(_) => true,
|
||||
Constant::Complex { .. } => true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_python_ast::{self as ast, Alias, Identifier, Int, Stmt};
|
||||
use ruff_python_ast::{self as ast, Alias, Identifier, Stmt};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
@@ -80,7 +80,7 @@ pub(crate) fn manual_from_import(
|
||||
asname: None,
|
||||
range: TextRange::default(),
|
||||
}],
|
||||
level: Some(Int::new(0)),
|
||||
level: Some(0),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
|
||||
@@ -71,7 +71,7 @@ pub(crate) fn deprecated_c_element_tree(checker: &mut Checker, stmt: &Stmt) {
|
||||
level,
|
||||
range: _,
|
||||
}) => {
|
||||
if level.is_some_and(|level| level.to_u32() > 0) {
|
||||
if level.is_some_and(|level| level > 0) {
|
||||
// Ex) `import .xml.etree.cElementTree as ET`
|
||||
} else if let Some(module) = module {
|
||||
if module == "xml.etree.cElementTree" {
|
||||
|
||||
@@ -323,7 +323,7 @@ pub(crate) fn deprecated_mock_import(checker: &mut Checker, stmt: &Stmt) {
|
||||
level,
|
||||
..
|
||||
}) => {
|
||||
if level.is_some_and(|level| level.to_u32() > 0) {
|
||||
if level.is_some_and(|level| level > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use num_bigint::BigInt;
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Constant, Expr};
|
||||
@@ -47,7 +45,7 @@ impl From<LiteralType> for Constant {
|
||||
value: Vec::new(),
|
||||
implicit_concatenated: false,
|
||||
}),
|
||||
LiteralType::Int => Constant::Int(BigInt::from(0)),
|
||||
LiteralType::Int => Constant::Int(0.into()),
|
||||
LiteralType::Float => Constant::Float(0.0),
|
||||
LiteralType::Bool => Constant::Bool(false),
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["", "EnvironmentError" | "IOError" | "WindowsError"]
|
||||
| ["mmap" | "select" | "socket", "error"]
|
||||
| ["mmap" | "select" | "socket" | "os", "error"]
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -93,16 +93,13 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
|
||||
}
|
||||
|
||||
/// Create a [`Diagnostic`] for a tuple of expressions.
|
||||
fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) {
|
||||
let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, target.range());
|
||||
fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) {
|
||||
let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, tuple.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if checker.semantic().is_builtin("OSError") {
|
||||
let Expr::Tuple(ast::ExprTuple { elts, .. }) = target else {
|
||||
panic!("Expected Expr::Tuple");
|
||||
};
|
||||
|
||||
// Filter out any `OSErrors` aliases.
|
||||
let mut remaining: Vec<Expr> = elts
|
||||
let mut remaining: Vec<Expr> = tuple
|
||||
.elts
|
||||
.iter()
|
||||
.filter_map(|elt| {
|
||||
if aliases.contains(&elt) {
|
||||
@@ -114,7 +111,11 @@ fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) {
|
||||
.collect();
|
||||
|
||||
// If `OSError` itself isn't already in the tuple, add it.
|
||||
if elts.iter().all(|elt| !is_os_error(elt, checker.semantic())) {
|
||||
if tuple
|
||||
.elts
|
||||
.iter()
|
||||
.all(|elt| !is_os_error(elt, checker.semantic()))
|
||||
{
|
||||
let node = ast::ExprName {
|
||||
id: "OSError".into(),
|
||||
ctx: ExprContext::Load,
|
||||
@@ -135,8 +136,8 @@ fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) {
|
||||
};
|
||||
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
pad(content, target.range(), checker.locator()),
|
||||
target.range(),
|
||||
pad(content, tuple.range(), checker.locator()),
|
||||
tuple.range(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -156,16 +157,16 @@ pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[ExceptH
|
||||
atom_diagnostic(checker, expr);
|
||||
}
|
||||
}
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
||||
Expr::Tuple(tuple) => {
|
||||
// List of aliases to replace with `OSError`.
|
||||
let mut aliases: Vec<&Expr> = vec![];
|
||||
for elt in elts {
|
||||
for elt in &tuple.elts {
|
||||
if is_alias(elt, checker.semantic()) {
|
||||
aliases.push(elt);
|
||||
}
|
||||
}
|
||||
if !aliases.is_empty() {
|
||||
tuple_diagnostic(checker, expr, &aliases);
|
||||
tuple_diagnostic(checker, tuple, &aliases);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use num_bigint::{BigInt, Sign};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use anyhow::Result;
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::stmt_if::{if_elif_branches, BranchKind, IfElifBranch};
|
||||
use ruff_python_ast::whitespace::indentation;
|
||||
use ruff_python_ast::{self as ast, CmpOp, Constant, ElifElseClause, Expr, StmtIf};
|
||||
use ruff_python_ast::{self as ast, CmpOp, Constant, ElifElseClause, Expr, Int, StmtIf};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
|
||||
use crate::autofix::edits::delete_stmt;
|
||||
@@ -47,19 +46,37 @@ use crate::settings::types::PythonVersion;
|
||||
/// ## References
|
||||
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
|
||||
#[violation]
|
||||
pub struct OutdatedVersionBlock;
|
||||
pub struct OutdatedVersionBlock {
|
||||
reason: Reason,
|
||||
}
|
||||
|
||||
impl Violation for OutdatedVersionBlock {
|
||||
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
|
||||
|
||||
impl AlwaysAutofixableViolation for OutdatedVersionBlock {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Version block is outdated for minimum Python version")
|
||||
let OutdatedVersionBlock { reason } = self;
|
||||
match reason {
|
||||
Reason::Outdated => format!("Version block is outdated for minimum Python version"),
|
||||
Reason::Invalid => format!("Version specifier is invalid"),
|
||||
}
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
"Remove outdated version block".to_string()
|
||||
fn autofix_title(&self) -> Option<String> {
|
||||
let OutdatedVersionBlock { reason } = self;
|
||||
match reason {
|
||||
Reason::Outdated => Some("Remove outdated version block".to_string()),
|
||||
Reason::Invalid => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Reason {
|
||||
Outdated,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// UP036
|
||||
pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
|
||||
for branch in if_elif_branches(stmt_if) {
|
||||
@@ -88,44 +105,19 @@ pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
|
||||
match comparison {
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => match op {
|
||||
CmpOp::Lt | CmpOp::LtE => {
|
||||
let version = extract_version(elts);
|
||||
let Some(version) = extract_version(elts) else {
|
||||
return;
|
||||
};
|
||||
let target = checker.settings.target_version;
|
||||
if compare_version(&version, target, op == &CmpOp::LtE) {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) = fix_always_false_branch(checker, stmt_if, &branch) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
CmpOp::Gt | CmpOp::GtE => {
|
||||
let version = extract_version(elts);
|
||||
let target = checker.settings.target_version;
|
||||
if compare_version(&version, target, op == &CmpOp::GtE) {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) = fix_always_true_branch(checker, stmt_if, &branch) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(number),
|
||||
..
|
||||
}) => {
|
||||
if op == &CmpOp::Eq {
|
||||
match bigint_to_u32(number) {
|
||||
2 => {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
|
||||
match compare_version(&version, target, op == &CmpOp::LtE) {
|
||||
Ok(false) => {}
|
||||
Ok(true) => {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
OutdatedVersionBlock {
|
||||
reason: Reason::Outdated,
|
||||
},
|
||||
branch.test.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) =
|
||||
fix_always_false_branch(checker, stmt_if, &branch)
|
||||
@@ -135,9 +127,30 @@ pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
3 => {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
|
||||
Err(_) => {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
OutdatedVersionBlock {
|
||||
reason: Reason::Invalid,
|
||||
},
|
||||
comparison.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
CmpOp::Gt | CmpOp::GtE => {
|
||||
let Some(version) = extract_version(elts) else {
|
||||
return;
|
||||
};
|
||||
let target = checker.settings.target_version;
|
||||
match compare_version(&version, target, op == &CmpOp::GtE) {
|
||||
Ok(false) => {}
|
||||
Ok(true) => {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
OutdatedVersionBlock {
|
||||
reason: Reason::Outdated,
|
||||
},
|
||||
branch.test.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) = fix_always_true_branch(checker, stmt_if, &branch)
|
||||
{
|
||||
@@ -146,6 +159,63 @@ pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
Err(_) => {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
OutdatedVersionBlock {
|
||||
reason: Reason::Invalid,
|
||||
},
|
||||
comparison.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(int),
|
||||
..
|
||||
}) => {
|
||||
if op == &CmpOp::Eq {
|
||||
match int.as_u8() {
|
||||
Some(2) => {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
OutdatedVersionBlock {
|
||||
reason: Reason::Outdated,
|
||||
},
|
||||
branch.test.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) =
|
||||
fix_always_false_branch(checker, stmt_if, &branch)
|
||||
{
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
Some(3) => {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
OutdatedVersionBlock {
|
||||
reason: Reason::Outdated,
|
||||
},
|
||||
branch.test.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) = fix_always_true_branch(checker, stmt_if, &branch)
|
||||
{
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
None => {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
OutdatedVersionBlock {
|
||||
reason: Reason::Invalid,
|
||||
},
|
||||
comparison.range(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -156,31 +226,42 @@ pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
|
||||
}
|
||||
|
||||
/// Returns true if the `target_version` is always less than the [`PythonVersion`].
|
||||
fn compare_version(target_version: &[u32], py_version: PythonVersion, or_equal: bool) -> bool {
|
||||
fn compare_version(
|
||||
target_version: &[Int],
|
||||
py_version: PythonVersion,
|
||||
or_equal: bool,
|
||||
) -> Result<bool> {
|
||||
let mut target_version_iter = target_version.iter();
|
||||
|
||||
let Some(if_major) = target_version_iter.next() else {
|
||||
return false;
|
||||
return Ok(false);
|
||||
};
|
||||
let Some(if_major) = if_major.as_u8() else {
|
||||
return Err(anyhow::anyhow!("invalid major version: {if_major}"));
|
||||
};
|
||||
|
||||
let (py_major, py_minor) = py_version.as_tuple();
|
||||
|
||||
match if_major.cmp(&py_major) {
|
||||
Ordering::Less => true,
|
||||
Ordering::Greater => false,
|
||||
Ordering::Less => Ok(true),
|
||||
Ordering::Greater => Ok(false),
|
||||
Ordering::Equal => {
|
||||
let Some(if_minor) = target_version_iter.next() else {
|
||||
return true;
|
||||
return Ok(true);
|
||||
};
|
||||
if or_equal {
|
||||
let Some(if_minor) = if_minor.as_u8() else {
|
||||
return Err(anyhow::anyhow!("invalid minor version: {if_minor}"));
|
||||
};
|
||||
|
||||
Ok(if or_equal {
|
||||
// Ex) `sys.version_info <= 3.8`. If Python 3.8 is the minimum supported version,
|
||||
// the condition won't always evaluate to `false`, so we want to return `false`.
|
||||
*if_minor < py_minor
|
||||
if_minor < py_minor
|
||||
} else {
|
||||
// Ex) `sys.version_info < 3.8`. If Python 3.8 is the minimum supported version,
|
||||
// the condition _will_ always evaluate to `false`, so we want to return `true`.
|
||||
*if_minor <= py_minor
|
||||
}
|
||||
if_minor <= py_minor
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,31 +434,20 @@ fn fix_always_true_branch(
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a `BigInt` to a `u32`. If the number is negative, it will return 0.
|
||||
fn bigint_to_u32(number: &BigInt) -> u32 {
|
||||
let the_number = number.to_u32_digits();
|
||||
match the_number.0 {
|
||||
Sign::Minus | Sign::NoSign => 0,
|
||||
Sign::Plus => *the_number.1.first().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the version from the tuple
|
||||
fn extract_version(elts: &[Expr]) -> Vec<u32> {
|
||||
let mut version: Vec<u32> = vec![];
|
||||
/// Return the version tuple as a sequence of [`Int`] values.
|
||||
fn extract_version(elts: &[Expr]) -> Option<Vec<Int>> {
|
||||
let mut version: Vec<Int> = vec![];
|
||||
for elt in elts {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(item),
|
||||
let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(int),
|
||||
..
|
||||
}) = &elt
|
||||
{
|
||||
let number = bigint_to_u32(item);
|
||||
version.push(number);
|
||||
} else {
|
||||
return version;
|
||||
}
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
version.push(int.clone());
|
||||
}
|
||||
version
|
||||
Some(version)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -399,10 +469,13 @@ mod tests {
|
||||
#[test_case(PythonVersion::Py310, &[3, 11], true, false; "compare-3.11")]
|
||||
fn test_compare_version(
|
||||
version: PythonVersion,
|
||||
version_vec: &[u32],
|
||||
target_versions: &[u8],
|
||||
or_equal: bool,
|
||||
expected: bool,
|
||||
) {
|
||||
assert_eq!(compare_version(version_vec, version, or_equal), expected);
|
||||
) -> Result<()> {
|
||||
let target_versions: Vec<_> = target_versions.iter().map(|int| Int::from(*int)).collect();
|
||||
let actual = compare_version(&target_versions, version, or_equal)?;
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,6 @@ use crate::autofix::edits::{pad, remove_argument, Parentheses};
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Reason {
|
||||
BytesLiteral,
|
||||
DefaultArgument,
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary calls to `encode` as UTF-8.
|
||||
///
|
||||
@@ -56,6 +50,12 @@ impl AlwaysAutofixableViolation for UnnecessaryEncodeUTF8 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Reason {
|
||||
BytesLiteral,
|
||||
DefaultArgument,
|
||||
}
|
||||
|
||||
const UTF8_LITERALS: &[&str] = &["utf-8", "utf8", "utf_8", "u8", "utf", "cp65001"];
|
||||
|
||||
fn match_encoded_variable(func: &Expr) -> Option<&Expr> {
|
||||
|
||||
@@ -281,5 +281,25 @@ UP024_0.py:105:11: UP024 [*] Replace aliased errors with `OSError`
|
||||
105 |- except(IOError, OSError) as ex:
|
||||
105 |+ except OSError as ex:
|
||||
106 106 | msg = 'Unable to query URL to get Owner ID: {u}\n{e}'.format(u=owner_id_url, e=ex)
|
||||
107 107 |
|
||||
108 108 |
|
||||
|
||||
UP024_0.py:114:8: UP024 [*] Replace aliased errors with `OSError`
|
||||
|
|
||||
112 | try:
|
||||
113 | pass
|
||||
114 | except os.error:
|
||||
| ^^^^^^^^ UP024
|
||||
115 | pass
|
||||
|
|
||||
= help: Replace `os.error` with builtin `OSError`
|
||||
|
||||
ℹ Fix
|
||||
111 111 |
|
||||
112 112 | try:
|
||||
113 113 | pass
|
||||
114 |-except os.error:
|
||||
114 |+except OSError:
|
||||
115 115 | pass
|
||||
|
||||
|
||||
|
||||
@@ -685,4 +685,31 @@ UP036_0.py:182:4: UP036 [*] Version block is outdated for minimum Python version
|
||||
185 183 | if sys.version_info <= (3,12):
|
||||
186 184 | print("py3")
|
||||
|
||||
UP036_0.py:191:24: UP036 Version specifier is invalid
|
||||
|
|
||||
189 | print("py3")
|
||||
190 |
|
||||
191 | if sys.version_info == 10000000:
|
||||
| ^^^^^^^^ UP036
|
||||
192 | print("py3")
|
||||
|
|
||||
|
||||
UP036_0.py:194:23: UP036 Version specifier is invalid
|
||||
|
|
||||
192 | print("py3")
|
||||
193 |
|
||||
194 | if sys.version_info < (3,10000000):
|
||||
| ^^^^^^^^^^^^ UP036
|
||||
195 | print("py3")
|
||||
|
|
||||
|
||||
UP036_0.py:197:24: UP036 Version specifier is invalid
|
||||
|
|
||||
195 | print("py3")
|
||||
196 |
|
||||
197 | if sys.version_info <= (3,10000000):
|
||||
| ^^^^^^^^^^^^ UP036
|
||||
198 | print("py3")
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ mod tests {
|
||||
#[test_case(Rule::ReimplementedStarmap, Path::new("FURB140.py"))]
|
||||
#[test_case(Rule::SliceCopy, Path::new("FURB145.py"))]
|
||||
#[test_case(Rule::UnnecessaryEnumerate, Path::new("FURB148.py"))]
|
||||
#[test_case(Rule::PrintEmptyString, Path::new("FURB105.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub(crate) use check_and_remove_from_set::*;
|
||||
pub(crate) use delete_full_slice::*;
|
||||
pub(crate) use print_empty_string::*;
|
||||
pub(crate) use reimplemented_starmap::*;
|
||||
pub(crate) use repeated_append::*;
|
||||
pub(crate) use slice_copy::*;
|
||||
@@ -7,6 +8,7 @@ pub(crate) use unnecessary_enumerate::*;
|
||||
|
||||
mod check_and_remove_from_set;
|
||||
mod delete_full_slice;
|
||||
mod print_empty_string;
|
||||
mod reimplemented_starmap;
|
||||
mod repeated_append;
|
||||
mod slice_copy;
|
||||
|
||||
229
crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs
Normal file
229
crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Constant, Expr};
|
||||
use ruff_python_codegen::Generator;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `print` calls with an empty string as the only positional
|
||||
/// argument.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Prefer calling `print` without any positional arguments, which is
|
||||
/// equivalent and more concise.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// print("")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// print()
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `print`](https://docs.python.org/3/library/functions.html#print)
|
||||
#[violation]
|
||||
pub struct PrintEmptyString {
|
||||
reason: Reason,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Reason {
|
||||
/// Ex) `print("")`
|
||||
EmptyArgument,
|
||||
/// Ex) `print("foo", sep="\t")`
|
||||
UselessSeparator,
|
||||
/// Ex) `print("", sep="\t")`
|
||||
Both,
|
||||
}
|
||||
|
||||
impl Violation for PrintEmptyString {
|
||||
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let PrintEmptyString { reason } = self;
|
||||
match reason {
|
||||
Reason::EmptyArgument => format!("Unnecessary empty string passed to `print`"),
|
||||
Reason::UselessSeparator => format!("Unnecessary separator passed to `print`"),
|
||||
Reason::Both => format!("Unnecessary empty string and separator passed to `print`"),
|
||||
}
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> Option<String> {
|
||||
let PrintEmptyString { reason } = self;
|
||||
match reason {
|
||||
Reason::EmptyArgument => Some("Remove empty string".to_string()),
|
||||
Reason::UselessSeparator => Some("Remove separator".to_string()),
|
||||
Reason::Both => Some("Remove empty string and separator".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FURB105
|
||||
pub(crate) fn print_empty_string(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
.as_ref()
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["", "print"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
match &call.arguments.args.as_slice() {
|
||||
// Ex) `print(*args)` or `print(*args, sep="\t")`
|
||||
[arg] if arg.is_starred_expr() => {}
|
||||
|
||||
// Ex) `print("")` or `print("", sep="\t")`
|
||||
[arg] if is_empty_string(arg) => {
|
||||
let reason = if call.arguments.find_keyword("sep").is_some() {
|
||||
Reason::Both
|
||||
} else {
|
||||
Reason::EmptyArgument
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(PrintEmptyString { reason }, call.range());
|
||||
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.set_fix(Fix::suggested(Edit::replacement(
|
||||
generate_suggestion(call, Separator::Remove, checker.generator()),
|
||||
call.start(),
|
||||
call.end(),
|
||||
)));
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
// Ex) `print(sep="\t")` or `print(obj, sep="\t")`
|
||||
[] | [_] => {
|
||||
// If there's a `sep` argument, remove it, regardless of what it is.
|
||||
if call.arguments.find_keyword("sep").is_some() {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
PrintEmptyString {
|
||||
reason: Reason::UselessSeparator,
|
||||
},
|
||||
call.range(),
|
||||
);
|
||||
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.set_fix(Fix::suggested(Edit::replacement(
|
||||
generate_suggestion(call, Separator::Remove, checker.generator()),
|
||||
call.start(),
|
||||
call.end(),
|
||||
)));
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
// Ex) `print("foo", "", "bar", sep="")`
|
||||
_ => {
|
||||
// Ignore `**kwargs`.
|
||||
let has_kwargs = call
|
||||
.arguments
|
||||
.keywords
|
||||
.iter()
|
||||
.any(|keyword| keyword.arg.is_none());
|
||||
if has_kwargs {
|
||||
return;
|
||||
}
|
||||
|
||||
// Require an empty `sep` argument.
|
||||
let empty_separator = call
|
||||
.arguments
|
||||
.find_keyword("sep")
|
||||
.map_or(false, |keyword| is_empty_string(&keyword.value));
|
||||
if !empty_separator {
|
||||
return;
|
||||
}
|
||||
|
||||
// Count the number of empty and non-empty arguments.
|
||||
let empty_arguments = call
|
||||
.arguments
|
||||
.args
|
||||
.iter()
|
||||
.filter(|arg| is_empty_string(arg))
|
||||
.count();
|
||||
if empty_arguments == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// If removing the arguments would leave us with one or fewer, then we can remove the
|
||||
// separator too.
|
||||
let separator = if call.arguments.args.len() - empty_arguments > 1
|
||||
|| call.arguments.args.iter().any(Expr::is_starred_expr)
|
||||
{
|
||||
Separator::Retain
|
||||
} else {
|
||||
Separator::Remove
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
PrintEmptyString {
|
||||
reason: if separator == Separator::Retain {
|
||||
Reason::EmptyArgument
|
||||
} else {
|
||||
Reason::Both
|
||||
},
|
||||
},
|
||||
call.range(),
|
||||
);
|
||||
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.set_fix(Fix::suggested(Edit::replacement(
|
||||
generate_suggestion(call, separator, checker.generator()),
|
||||
call.start(),
|
||||
call.end(),
|
||||
)));
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an expression is a constant empty string.
|
||||
fn is_empty_string(expr: &Expr) -> bool {
|
||||
matches!(
|
||||
expr,
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
}) if value.is_empty()
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Separator {
|
||||
Remove,
|
||||
Retain,
|
||||
}
|
||||
|
||||
/// Generate a suggestion to remove the empty string positional argument and
|
||||
/// the `sep` keyword argument, if it exists.
|
||||
fn generate_suggestion(call: &ast::ExprCall, separator: Separator, generator: Generator) -> String {
|
||||
let mut call = call.clone();
|
||||
|
||||
// Remove all empty string positional arguments.
|
||||
call.arguments.args.retain(|arg| !is_empty_string(arg));
|
||||
|
||||
// Remove the `sep` keyword argument if it exists.
|
||||
if separator == Separator::Remove {
|
||||
call.arguments.keywords.retain(|keyword| {
|
||||
keyword
|
||||
.arg
|
||||
.as_ref()
|
||||
.map_or(true, |arg| arg.as_str() != "sep")
|
||||
});
|
||||
}
|
||||
|
||||
generator.expr(&call.into())
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
use std::fmt;
|
||||
|
||||
use num_traits::Zero;
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{Arguments, Constant, Expr};
|
||||
use ruff_python_ast::{Arguments, Constant, Expr, Int};
|
||||
use ruff_python_codegen::Generator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
@@ -160,15 +158,13 @@ pub(crate) fn unnecessary_enumerate(checker: &mut Checker, stmt_for: &ast::StmtF
|
||||
// there's no clear fix.
|
||||
let start = arguments.find_argument("start", 1);
|
||||
if start.map_or(true, |start| {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(value),
|
||||
..
|
||||
}) = start
|
||||
{
|
||||
value.is_zero()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
matches!(
|
||||
start,
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(Int::ZERO),
|
||||
..
|
||||
})
|
||||
)
|
||||
}) {
|
||||
let replace_iter = Edit::range_replacement(
|
||||
generate_range_len_call(sequence, checker.generator()),
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB105.py:3:1: FURB105 [*] Unnecessary empty string passed to `print`
|
||||
|
|
||||
1 | # Errors.
|
||||
2 |
|
||||
3 | print("")
|
||||
| ^^^^^^^^^ FURB105
|
||||
4 | print("", sep=",")
|
||||
5 | print("", end="bar")
|
||||
|
|
||||
= help: Remove empty string
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | # Errors.
|
||||
2 2 |
|
||||
3 |-print("")
|
||||
3 |+print()
|
||||
4 4 | print("", sep=",")
|
||||
5 5 | print("", end="bar")
|
||||
6 6 | print("", sep=",", end="bar")
|
||||
|
||||
FURB105.py:4:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
||||
|
|
||||
3 | print("")
|
||||
4 | print("", sep=",")
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB105
|
||||
5 | print("", end="bar")
|
||||
6 | print("", sep=",", end="bar")
|
||||
|
|
||||
= help: Remove empty string and separator
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | # Errors.
|
||||
2 2 |
|
||||
3 3 | print("")
|
||||
4 |-print("", sep=",")
|
||||
4 |+print()
|
||||
5 5 | print("", end="bar")
|
||||
6 6 | print("", sep=",", end="bar")
|
||||
7 7 | print(sep="")
|
||||
|
||||
FURB105.py:5:1: FURB105 [*] Unnecessary empty string passed to `print`
|
||||
|
|
||||
3 | print("")
|
||||
4 | print("", sep=",")
|
||||
5 | print("", end="bar")
|
||||
| ^^^^^^^^^^^^^^^^^^^^ FURB105
|
||||
6 | print("", sep=",", end="bar")
|
||||
7 | print(sep="")
|
||||
|
|
||||
= help: Remove empty string
|
||||
|
||||
ℹ Suggested fix
|
||||
2 2 |
|
||||
3 3 | print("")
|
||||
4 4 | print("", sep=",")
|
||||
5 |-print("", end="bar")
|
||||
5 |+print(end="bar")
|
||||
6 6 | print("", sep=",", end="bar")
|
||||
7 7 | print(sep="")
|
||||
8 8 | print("", sep="")
|
||||
|
||||
FURB105.py:6:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
||||
|
|
||||
4 | print("", sep=",")
|
||||
5 | print("", end="bar")
|
||||
6 | print("", sep=",", end="bar")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
|
||||
7 | print(sep="")
|
||||
8 | print("", sep="")
|
||||
|
|
||||
= help: Remove empty string and separator
|
||||
|
||||
ℹ Suggested fix
|
||||
3 3 | print("")
|
||||
4 4 | print("", sep=",")
|
||||
5 5 | print("", end="bar")
|
||||
6 |-print("", sep=",", end="bar")
|
||||
6 |+print(end="bar")
|
||||
7 7 | print(sep="")
|
||||
8 8 | print("", sep="")
|
||||
9 9 | print("", "", sep="")
|
||||
|
||||
FURB105.py:7:1: FURB105 [*] Unnecessary separator passed to `print`
|
||||
|
|
||||
5 | print("", end="bar")
|
||||
6 | print("", sep=",", end="bar")
|
||||
7 | print(sep="")
|
||||
| ^^^^^^^^^^^^^ FURB105
|
||||
8 | print("", sep="")
|
||||
9 | print("", "", sep="")
|
||||
|
|
||||
= help: Remove separator
|
||||
|
||||
ℹ Suggested fix
|
||||
4 4 | print("", sep=",")
|
||||
5 5 | print("", end="bar")
|
||||
6 6 | print("", sep=",", end="bar")
|
||||
7 |-print(sep="")
|
||||
7 |+print()
|
||||
8 8 | print("", sep="")
|
||||
9 9 | print("", "", sep="")
|
||||
10 10 | print("", "", sep="", end="")
|
||||
|
||||
FURB105.py:8:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
||||
|
|
||||
6 | print("", sep=",", end="bar")
|
||||
7 | print(sep="")
|
||||
8 | print("", sep="")
|
||||
| ^^^^^^^^^^^^^^^^^ FURB105
|
||||
9 | print("", "", sep="")
|
||||
10 | print("", "", sep="", end="")
|
||||
|
|
||||
= help: Remove empty string and separator
|
||||
|
||||
ℹ Suggested fix
|
||||
5 5 | print("", end="bar")
|
||||
6 6 | print("", sep=",", end="bar")
|
||||
7 7 | print(sep="")
|
||||
8 |-print("", sep="")
|
||||
8 |+print()
|
||||
9 9 | print("", "", sep="")
|
||||
10 10 | print("", "", sep="", end="")
|
||||
11 11 | print("", "", sep="", end="bar")
|
||||
|
||||
FURB105.py:9:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
||||
|
|
||||
7 | print(sep="")
|
||||
8 | print("", sep="")
|
||||
9 | print("", "", sep="")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ FURB105
|
||||
10 | print("", "", sep="", end="")
|
||||
11 | print("", "", sep="", end="bar")
|
||||
|
|
||||
= help: Remove empty string and separator
|
||||
|
||||
ℹ Suggested fix
|
||||
6 6 | print("", sep=",", end="bar")
|
||||
7 7 | print(sep="")
|
||||
8 8 | print("", sep="")
|
||||
9 |-print("", "", sep="")
|
||||
9 |+print()
|
||||
10 10 | print("", "", sep="", end="")
|
||||
11 11 | print("", "", sep="", end="bar")
|
||||
12 12 | print("", sep="", end="bar")
|
||||
|
||||
FURB105.py:10:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
||||
|
|
||||
8 | print("", sep="")
|
||||
9 | print("", "", sep="")
|
||||
10 | print("", "", sep="", end="")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
|
||||
11 | print("", "", sep="", end="bar")
|
||||
12 | print("", sep="", end="bar")
|
||||
|
|
||||
= help: Remove empty string and separator
|
||||
|
||||
ℹ Suggested fix
|
||||
7 7 | print(sep="")
|
||||
8 8 | print("", sep="")
|
||||
9 9 | print("", "", sep="")
|
||||
10 |-print("", "", sep="", end="")
|
||||
10 |+print(end="")
|
||||
11 11 | print("", "", sep="", end="bar")
|
||||
12 12 | print("", sep="", end="bar")
|
||||
13 13 | print(sep="", end="bar")
|
||||
|
||||
FURB105.py:11:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
||||
|
|
||||
9 | print("", "", sep="")
|
||||
10 | print("", "", sep="", end="")
|
||||
11 | print("", "", sep="", end="bar")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
|
||||
12 | print("", sep="", end="bar")
|
||||
13 | print(sep="", end="bar")
|
||||
|
|
||||
= help: Remove empty string and separator
|
||||
|
||||
ℹ Suggested fix
|
||||
8 8 | print("", sep="")
|
||||
9 9 | print("", "", sep="")
|
||||
10 10 | print("", "", sep="", end="")
|
||||
11 |-print("", "", sep="", end="bar")
|
||||
11 |+print(end="bar")
|
||||
12 12 | print("", sep="", end="bar")
|
||||
13 13 | print(sep="", end="bar")
|
||||
14 14 | print("", "foo", sep="")
|
||||
|
||||
FURB105.py:12:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
||||
|
|
||||
10 | print("", "", sep="", end="")
|
||||
11 | print("", "", sep="", end="bar")
|
||||
12 | print("", sep="", end="bar")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
|
||||
13 | print(sep="", end="bar")
|
||||
14 | print("", "foo", sep="")
|
||||
|
|
||||
= help: Remove empty string and separator
|
||||
|
||||
ℹ Suggested fix
|
||||
9 9 | print("", "", sep="")
|
||||
10 10 | print("", "", sep="", end="")
|
||||
11 11 | print("", "", sep="", end="bar")
|
||||
12 |-print("", sep="", end="bar")
|
||||
12 |+print(end="bar")
|
||||
13 13 | print(sep="", end="bar")
|
||||
14 14 | print("", "foo", sep="")
|
||||
15 15 | print("foo", "", sep="")
|
||||
|
||||
FURB105.py:13:1: FURB105 [*] Unnecessary separator passed to `print`
|
||||
|
|
||||
11 | print("", "", sep="", end="bar")
|
||||
12 | print("", sep="", end="bar")
|
||||
13 | print(sep="", end="bar")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
|
||||
14 | print("", "foo", sep="")
|
||||
15 | print("foo", "", sep="")
|
||||
|
|
||||
= help: Remove separator
|
||||
|
||||
ℹ Suggested fix
|
||||
10 10 | print("", "", sep="", end="")
|
||||
11 11 | print("", "", sep="", end="bar")
|
||||
12 12 | print("", sep="", end="bar")
|
||||
13 |-print(sep="", end="bar")
|
||||
13 |+print(end="bar")
|
||||
14 14 | print("", "foo", sep="")
|
||||
15 15 | print("foo", "", sep="")
|
||||
16 16 | print("foo", "", "bar", sep="")
|
||||
|
||||
FURB105.py:14:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
||||
|
|
||||
12 | print("", sep="", end="bar")
|
||||
13 | print(sep="", end="bar")
|
||||
14 | print("", "foo", sep="")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
|
||||
15 | print("foo", "", sep="")
|
||||
16 | print("foo", "", "bar", sep="")
|
||||
|
|
||||
= help: Remove empty string and separator
|
||||
|
||||
ℹ Suggested fix
|
||||
11 11 | print("", "", sep="", end="bar")
|
||||
12 12 | print("", sep="", end="bar")
|
||||
13 13 | print(sep="", end="bar")
|
||||
14 |-print("", "foo", sep="")
|
||||
14 |+print("foo")
|
||||
15 15 | print("foo", "", sep="")
|
||||
16 16 | print("foo", "", "bar", sep="")
|
||||
17 17 | print("", *args)
|
||||
|
||||
FURB105.py:15:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
||||
|
|
||||
13 | print(sep="", end="bar")
|
||||
14 | print("", "foo", sep="")
|
||||
15 | print("foo", "", sep="")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
|
||||
16 | print("foo", "", "bar", sep="")
|
||||
17 | print("", *args)
|
||||
|
|
||||
= help: Remove empty string and separator
|
||||
|
||||
ℹ Suggested fix
|
||||
12 12 | print("", sep="", end="bar")
|
||||
13 13 | print(sep="", end="bar")
|
||||
14 14 | print("", "foo", sep="")
|
||||
15 |-print("foo", "", sep="")
|
||||
15 |+print("foo")
|
||||
16 16 | print("foo", "", "bar", sep="")
|
||||
17 17 | print("", *args)
|
||||
18 18 | print("", *args, sep="")
|
||||
|
||||
FURB105.py:16:1: FURB105 [*] Unnecessary empty string passed to `print`
|
||||
|
|
||||
14 | print("", "foo", sep="")
|
||||
15 | print("foo", "", sep="")
|
||||
16 | print("foo", "", "bar", sep="")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
|
||||
17 | print("", *args)
|
||||
18 | print("", *args, sep="")
|
||||
|
|
||||
= help: Remove empty string
|
||||
|
||||
ℹ Suggested fix
|
||||
13 13 | print(sep="", end="bar")
|
||||
14 14 | print("", "foo", sep="")
|
||||
15 15 | print("foo", "", sep="")
|
||||
16 |-print("foo", "", "bar", sep="")
|
||||
16 |+print("foo", "bar", sep="")
|
||||
17 17 | print("", *args)
|
||||
18 18 | print("", *args, sep="")
|
||||
19 19 | print("", **kwargs)
|
||||
|
||||
FURB105.py:18:1: FURB105 [*] Unnecessary empty string passed to `print`
|
||||
|
|
||||
16 | print("foo", "", "bar", sep="")
|
||||
17 | print("", *args)
|
||||
18 | print("", *args, sep="")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
|
||||
19 | print("", **kwargs)
|
||||
20 | print(sep="\t")
|
||||
|
|
||||
= help: Remove empty string
|
||||
|
||||
ℹ Suggested fix
|
||||
15 15 | print("foo", "", sep="")
|
||||
16 16 | print("foo", "", "bar", sep="")
|
||||
17 17 | print("", *args)
|
||||
18 |-print("", *args, sep="")
|
||||
18 |+print(*args, sep="")
|
||||
19 19 | print("", **kwargs)
|
||||
20 20 | print(sep="\t")
|
||||
21 21 |
|
||||
|
||||
FURB105.py:19:1: FURB105 [*] Unnecessary empty string passed to `print`
|
||||
|
|
||||
17 | print("", *args)
|
||||
18 | print("", *args, sep="")
|
||||
19 | print("", **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^ FURB105
|
||||
20 | print(sep="\t")
|
||||
|
|
||||
= help: Remove empty string
|
||||
|
||||
ℹ Suggested fix
|
||||
16 16 | print("foo", "", "bar", sep="")
|
||||
17 17 | print("", *args)
|
||||
18 18 | print("", *args, sep="")
|
||||
19 |-print("", **kwargs)
|
||||
19 |+print(**kwargs)
|
||||
20 20 | print(sep="\t")
|
||||
21 21 |
|
||||
22 22 | # OK.
|
||||
|
||||
FURB105.py:20:1: FURB105 [*] Unnecessary separator passed to `print`
|
||||
|
|
||||
18 | print("", *args, sep="")
|
||||
19 | print("", **kwargs)
|
||||
20 | print(sep="\t")
|
||||
| ^^^^^^^^^^^^^^^ FURB105
|
||||
21 |
|
||||
22 | # OK.
|
||||
|
|
||||
= help: Remove separator
|
||||
|
||||
ℹ Suggested fix
|
||||
17 17 | print("", *args)
|
||||
18 18 | print("", *args, sep="")
|
||||
19 19 | print("", **kwargs)
|
||||
20 |-print(sep="\t")
|
||||
20 |+print()
|
||||
21 21 |
|
||||
22 22 | # OK.
|
||||
23 23 |
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use num_traits::ToPrimitive;
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, UnaryOp};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, Int};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -45,26 +43,17 @@ impl Violation for PairwiseOverZipped {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SliceInfo {
|
||||
arg_name: String,
|
||||
slice_start: Option<i64>,
|
||||
id: String,
|
||||
slice_start: Option<i32>,
|
||||
}
|
||||
|
||||
impl SliceInfo {
|
||||
pub(crate) fn new(arg_name: String, slice_start: Option<i64>) -> Self {
|
||||
Self {
|
||||
arg_name,
|
||||
slice_start,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the argument name, lower bound, and upper bound for an expression, if it's a slice.
|
||||
/// Return the argument name, lower bound, and upper bound for an expression, if it's a slice.
|
||||
fn match_slice_info(expr: &Expr) -> Option<SliceInfo> {
|
||||
let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Expr::Name(ast::ExprName { id: arg_id, .. }) = value.as_ref() else {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
@@ -74,44 +63,40 @@ fn match_slice_info(expr: &Expr) -> Option<SliceInfo> {
|
||||
|
||||
// Avoid false positives for slices with a step.
|
||||
if let Some(step) = step {
|
||||
if let Some(step) = to_bound(step) {
|
||||
if step != 1 {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
if !matches!(
|
||||
step.as_ref(),
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(Int::ONE),
|
||||
..
|
||||
})
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(SliceInfo::new(
|
||||
arg_id.to_string(),
|
||||
lower.as_ref().and_then(|expr| to_bound(expr)),
|
||||
))
|
||||
}
|
||||
|
||||
fn to_bound(expr: &Expr) -> Option<i64> {
|
||||
match expr {
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(value),
|
||||
..
|
||||
}) => value.to_i64(),
|
||||
Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: UnaryOp::USub | UnaryOp::Invert,
|
||||
operand,
|
||||
// If the slice start is a non-constant, we can't be sure that it's successive.
|
||||
let slice_start = if let Some(lower) = lower.as_ref() {
|
||||
let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(int),
|
||||
range: _,
|
||||
}) => {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(value),
|
||||
..
|
||||
}) = operand.as_ref()
|
||||
{
|
||||
value.to_i64().map(|v| -v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}) = lower.as_ref()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Some(slice_start) = int.as_i32() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(slice_start)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Some(SliceInfo {
|
||||
id: id.to_string(),
|
||||
slice_start,
|
||||
})
|
||||
}
|
||||
|
||||
/// RUF007
|
||||
@@ -121,9 +106,9 @@ pub(crate) fn pairwise_over_zipped(checker: &mut Checker, func: &Expr, args: &[E
|
||||
};
|
||||
|
||||
// Require exactly two positional arguments.
|
||||
if args.len() != 2 {
|
||||
let [first, second] = args else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Require the function to be the builtin `zip`.
|
||||
if !(id == "zip" && checker.semantic().is_builtin(id)) {
|
||||
@@ -132,25 +117,28 @@ pub(crate) fn pairwise_over_zipped(checker: &mut Checker, func: &Expr, args: &[E
|
||||
|
||||
// Allow the first argument to be a `Name` or `Subscript`.
|
||||
let Some(first_arg_info) = ({
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = &args[0] {
|
||||
Some(SliceInfo::new(id.to_string(), None))
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = first {
|
||||
Some(SliceInfo {
|
||||
id: id.to_string(),
|
||||
slice_start: None,
|
||||
})
|
||||
} else {
|
||||
match_slice_info(&args[0])
|
||||
match_slice_info(first)
|
||||
}
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Require second argument to be a `Subscript`.
|
||||
if !args[1].is_subscript_expr() {
|
||||
if !second.is_subscript_expr() {
|
||||
return;
|
||||
}
|
||||
let Some(second_arg_info) = match_slice_info(&args[1]) else {
|
||||
let Some(second_arg_info) = match_slice_info(second) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Verify that the arguments match the same name.
|
||||
if first_arg_info.arg_name != second_arg_info.arg_name {
|
||||
if first_arg_info.id != second_arg_info.id {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use num_traits::Zero;
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Arguments, Comprehension, Constant, Expr};
|
||||
use ruff_python_ast::{self as ast, Arguments, Comprehension, Constant, Expr, Int};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
@@ -110,15 +108,13 @@ pub(crate) fn unnecessary_iterable_allocation_for_first_element(
|
||||
|
||||
/// Check that the slice [`Expr`] is a slice of the first element (e.g., `x[0]`).
|
||||
fn is_head_slice(expr: &Expr) -> bool {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(value),
|
||||
..
|
||||
}) = expr
|
||||
{
|
||||
value.is_zero()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
matches!(
|
||||
expr,
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(Int::ZERO),
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -11,7 +11,7 @@ use ruff_source_file::Locator;
|
||||
///
|
||||
/// A known type is either a builtin type, any object from the standard library,
|
||||
/// or a type from the `typing_extensions` module.
|
||||
fn is_known_type(call_path: &CallPath, minor_version: u32) -> bool {
|
||||
fn is_known_type(call_path: &CallPath, minor_version: u8) -> bool {
|
||||
match call_path.as_slice() {
|
||||
["" | "typing_extensions", ..] => true,
|
||||
[module, ..] => is_known_standard_library(minor_version, module),
|
||||
@@ -72,7 +72,7 @@ impl<'a> TypingTarget<'a> {
|
||||
expr: &'a Expr,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
minor_version: u32,
|
||||
minor_version: u8,
|
||||
) -> Option<Self> {
|
||||
match expr {
|
||||
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||
@@ -141,7 +141,7 @@ impl<'a> TypingTarget<'a> {
|
||||
&self,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
minor_version: u32,
|
||||
minor_version: u8,
|
||||
) -> bool {
|
||||
match self {
|
||||
TypingTarget::None
|
||||
@@ -189,12 +189,7 @@ impl<'a> TypingTarget<'a> {
|
||||
}
|
||||
|
||||
/// Check if the [`TypingTarget`] explicitly allows `Any`.
|
||||
fn contains_any(
|
||||
&self,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
minor_version: u32,
|
||||
) -> bool {
|
||||
fn contains_any(&self, semantic: &SemanticModel, locator: &Locator, minor_version: u8) -> bool {
|
||||
match self {
|
||||
TypingTarget::Any => true,
|
||||
// `Literal` cannot contain `Any` as it's a dynamic value.
|
||||
@@ -242,7 +237,7 @@ pub(crate) fn type_hint_explicitly_allows_none<'a>(
|
||||
annotation: &'a Expr,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
minor_version: u32,
|
||||
minor_version: u8,
|
||||
) -> Option<&'a Expr> {
|
||||
match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) {
|
||||
None |
|
||||
@@ -272,7 +267,7 @@ pub(crate) fn type_hint_resolves_to_any(
|
||||
annotation: &Expr,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
minor_version: u32,
|
||||
minor_version: u8,
|
||||
) -> bool {
|
||||
match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) {
|
||||
None |
|
||||
|
||||
@@ -58,7 +58,7 @@ impl PythonVersion {
|
||||
Self::Py312
|
||||
}
|
||||
|
||||
pub const fn as_tuple(&self) -> (u32, u32) {
|
||||
pub const fn as_tuple(&self) -> (u8, u8) {
|
||||
match self {
|
||||
Self::Py37 => (3, 7),
|
||||
Self::Py38 => (3, 8),
|
||||
@@ -69,11 +69,11 @@ impl PythonVersion {
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn major(&self) -> u32 {
|
||||
pub const fn major(&self) -> u8 {
|
||||
self.as_tuple().0
|
||||
}
|
||||
|
||||
pub const fn minor(&self) -> u32 {
|
||||
pub const fn minor(&self) -> u8 {
|
||||
self.as_tuple().1
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,12 @@ use syn::{
|
||||
};
|
||||
|
||||
pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
||||
let DeriveInput { ident, data, .. } = input;
|
||||
let DeriveInput {
|
||||
ident,
|
||||
data,
|
||||
attrs: struct_attributes,
|
||||
..
|
||||
} = input;
|
||||
|
||||
match data {
|
||||
Data::Struct(DataStruct {
|
||||
@@ -50,15 +55,39 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenS
|
||||
};
|
||||
}
|
||||
|
||||
let options_len = output.len();
|
||||
let docs: Vec<&Attribute> = struct_attributes
|
||||
.iter()
|
||||
.filter(|attr| attr.path().is_ident("doc"))
|
||||
.collect();
|
||||
|
||||
// Convert the list of `doc` attributes into a single string.
|
||||
let doc = dedent(
|
||||
&docs
|
||||
.into_iter()
|
||||
.map(parse_doc)
|
||||
.collect::<syn::Result<Vec<_>>>()?
|
||||
.join("\n"),
|
||||
)
|
||||
.trim_matches('\n')
|
||||
.to_string();
|
||||
|
||||
let documentation = if doc.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(quote!(
|
||||
fn documentation() -> Option<&'static str> {
|
||||
Some(&#doc)
|
||||
}
|
||||
))
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
|
||||
impl #ident {
|
||||
pub const fn metadata() -> crate::options_base::OptionGroup {
|
||||
const OPTIONS: [(&'static str, crate::options_base::OptionEntry); #options_len] = [#(#output),*];
|
||||
crate::options_base::OptionGroup::new(&OPTIONS)
|
||||
impl crate::options_base::OptionsMetadata for #ident {
|
||||
fn record(visit: &mut dyn crate::options_base::Visit) {
|
||||
#(#output);*
|
||||
}
|
||||
|
||||
#documentation
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -92,7 +121,7 @@ fn handle_option_group(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
|
||||
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
|
||||
|
||||
Ok(quote_spanned!(
|
||||
ident.span() => (#kebab_name, crate::options_base::OptionEntry::Group(#path::metadata()))
|
||||
ident.span() => (visit.record_set(#kebab_name, crate::options_base::OptionSet::of::<#path>()))
|
||||
))
|
||||
}
|
||||
_ => Err(syn::Error::new(
|
||||
@@ -150,12 +179,14 @@ fn handle_option(
|
||||
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
|
||||
|
||||
Ok(quote_spanned!(
|
||||
ident.span() => (#kebab_name, crate::options_base::OptionEntry::Field(crate::options_base::OptionField {
|
||||
doc: &#doc,
|
||||
default: &#default,
|
||||
value_type: &#value_type,
|
||||
example: &#example,
|
||||
}))
|
||||
ident.span() => {
|
||||
visit.record_field(#kebab_name, crate::options_base::OptionField{
|
||||
doc: &#doc,
|
||||
default: &#default,
|
||||
value_type: &#value_type,
|
||||
example: &#example,
|
||||
})
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@ bitflags = { workspace = true }
|
||||
is-macro = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
num-bigint = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
//! an implicit concatenation of string literals, as these expressions are considered to
|
||||
//! have the same shape in that they evaluate to the same value.
|
||||
|
||||
use num_bigint::BigInt;
|
||||
|
||||
use crate as ast;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
||||
@@ -334,7 +332,7 @@ pub enum ComparableConstant<'a> {
|
||||
Bool(&'a bool),
|
||||
Str { value: &'a str, unicode: bool },
|
||||
Bytes(&'a [u8]),
|
||||
Int(&'a BigInt),
|
||||
Int(&'a ast::Int),
|
||||
Tuple(Vec<ComparableConstant<'a>>),
|
||||
Float(u64),
|
||||
Complex { real: u64, imag: u64 },
|
||||
@@ -1161,7 +1159,7 @@ pub struct StmtImport<'a> {
|
||||
pub struct StmtImportFrom<'a> {
|
||||
module: Option<&'a str>,
|
||||
names: Vec<ComparableAlias<'a>>,
|
||||
level: Option<ast::Int>,
|
||||
level: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
|
||||
use num_traits::Zero;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
@@ -1073,7 +1072,7 @@ impl Truthiness {
|
||||
Constant::None => Some(false),
|
||||
Constant::Str(ast::StringConstant { value, .. }) => Some(!value.is_empty()),
|
||||
Constant::Bytes(bytes) => Some(!bytes.is_empty()),
|
||||
Constant::Int(int) => Some(!int.is_zero()),
|
||||
Constant::Int(int) => Some(*int != 0),
|
||||
Constant::Float(float) => Some(*float != 0.0),
|
||||
Constant::Complex { real, imag } => Some(*real != 0.0 || *imag != 0.0),
|
||||
Constant::Ellipsis => Some(true),
|
||||
@@ -1140,7 +1139,7 @@ mod tests {
|
||||
|
||||
use crate::helpers::{any_over_stmt, any_over_type_param, resolve_imported_module_path};
|
||||
use crate::{
|
||||
Constant, Expr, ExprConstant, ExprContext, ExprName, Identifier, Stmt, StmtTypeAlias,
|
||||
Constant, Expr, ExprConstant, ExprContext, ExprName, Identifier, Int, Stmt, StmtTypeAlias,
|
||||
TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, TypeParams,
|
||||
};
|
||||
|
||||
@@ -1240,7 +1239,7 @@ mod tests {
|
||||
assert!(!any_over_type_param(&type_var_no_bound, &|_expr| true));
|
||||
|
||||
let bound = Expr::Constant(ExprConstant {
|
||||
value: Constant::Int(1.into()),
|
||||
value: Constant::Int(Int::ONE),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
|
||||
228
crates/ruff_python_ast/src/int.rs
Normal file
228
crates/ruff_python_ast/src/int.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use std::fmt::Debug;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// A Python integer literal. Represents both small (fits in an `i64`) and large integers.
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Int(Number);
|
||||
|
||||
impl FromStr for Int {
|
||||
type Err = std::num::ParseIntError;
|
||||
|
||||
/// Parse an [`Int`] from a string.
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.parse::<i64>() {
|
||||
Ok(value) => Ok(Int::small(value)),
|
||||
Err(err) => {
|
||||
if matches!(
|
||||
err.kind(),
|
||||
std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow
|
||||
) {
|
||||
Ok(Int::big(s))
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Int {
|
||||
pub const ZERO: Int = Int(Number::Small(0));
|
||||
pub const ONE: Int = Int(Number::Small(1));
|
||||
|
||||
/// Create an [`Int`] to represent a value that can be represented as an `i64`.
|
||||
fn small(value: i64) -> Self {
|
||||
Self(Number::Small(value))
|
||||
}
|
||||
|
||||
/// Create an [`Int`] to represent a value that cannot be represented as an `i64`.
|
||||
fn big(value: impl Into<Box<str>>) -> Self {
|
||||
Self(Number::Big(value.into()))
|
||||
}
|
||||
|
||||
/// Parse an [`Int`] from a string with a given radix.
|
||||
pub fn from_str_radix(s: &str, radix: u32) -> Result<Self, std::num::ParseIntError> {
|
||||
match i64::from_str_radix(s, radix) {
|
||||
Ok(value) => Ok(Int::small(value)),
|
||||
Err(err) => {
|
||||
if matches!(
|
||||
err.kind(),
|
||||
std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow
|
||||
) {
|
||||
Ok(Int::big(s))
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an u8, if it can be represented as that data type.
|
||||
pub fn as_u8(&self) -> Option<u8> {
|
||||
match &self.0 {
|
||||
Number::Small(small) => u8::try_from(*small).ok(),
|
||||
Number::Big(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an u16, if it can be represented as that data type.
|
||||
pub fn as_u16(&self) -> Option<u16> {
|
||||
match &self.0 {
|
||||
Number::Small(small) => u16::try_from(*small).ok(),
|
||||
Number::Big(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an u32, if it can be represented as that data type.
|
||||
pub fn as_u32(&self) -> Option<u32> {
|
||||
match &self.0 {
|
||||
Number::Small(small) => u32::try_from(*small).ok(),
|
||||
Number::Big(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an i8, if it can be represented as that data type.
|
||||
pub fn as_i8(&self) -> Option<i8> {
|
||||
match &self.0 {
|
||||
Number::Small(small) => i8::try_from(*small).ok(),
|
||||
Number::Big(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an i16, if it can be represented as that data type.
|
||||
pub fn as_i16(&self) -> Option<i16> {
|
||||
match &self.0 {
|
||||
Number::Small(small) => i16::try_from(*small).ok(),
|
||||
Number::Big(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an i32, if it can be represented as that data type.
|
||||
pub fn as_i32(&self) -> Option<i32> {
|
||||
match &self.0 {
|
||||
Number::Small(small) => i32::try_from(*small).ok(),
|
||||
Number::Big(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an i64, if it can be represented as that data type.
|
||||
pub const fn as_i64(&self) -> Option<i64> {
|
||||
match &self.0 {
|
||||
Number::Small(small) => Some(*small),
|
||||
Number::Big(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Int {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Int {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<u8> for Int {
|
||||
fn eq(&self, other: &u8) -> bool {
|
||||
self.as_u8() == Some(*other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<u16> for Int {
|
||||
fn eq(&self, other: &u16) -> bool {
|
||||
self.as_u16() == Some(*other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<u32> for Int {
|
||||
fn eq(&self, other: &u32) -> bool {
|
||||
self.as_u32() == Some(*other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<i8> for Int {
|
||||
fn eq(&self, other: &i8) -> bool {
|
||||
self.as_i8() == Some(*other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<i16> for Int {
|
||||
fn eq(&self, other: &i16) -> bool {
|
||||
self.as_i16() == Some(*other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<i32> for Int {
|
||||
fn eq(&self, other: &i32) -> bool {
|
||||
self.as_i32() == Some(*other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<i64> for Int {
|
||||
fn eq(&self, other: &i64) -> bool {
|
||||
self.as_i64() == Some(*other)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for Int {
|
||||
fn from(value: u8) -> Self {
|
||||
Self::small(i64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Int {
|
||||
fn from(value: u16) -> Self {
|
||||
Self::small(i64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Int {
|
||||
fn from(value: u32) -> Self {
|
||||
Self::small(i64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i8> for Int {
|
||||
fn from(value: i8) -> Self {
|
||||
Self::small(i64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i16> for Int {
|
||||
fn from(value: i16) -> Self {
|
||||
Self::small(i64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Int {
|
||||
fn from(value: i32) -> Self {
|
||||
Self::small(i64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Int {
|
||||
fn from(value: i64) -> Self {
|
||||
Self::small(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
enum Number {
|
||||
/// A "small" number that can be represented as an `i64`.
|
||||
Small(i64),
|
||||
/// A "large" number that cannot be represented as an `i64`.
|
||||
Big(Box<str>),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Number {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Number::Small(value) => write!(f, "{value}"),
|
||||
Number::Big(value) => write!(f, "{value}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
pub use expression::*;
|
||||
pub use int::*;
|
||||
pub use nodes::*;
|
||||
|
||||
pub mod all;
|
||||
@@ -12,6 +13,7 @@ pub mod hashable;
|
||||
pub mod helpers;
|
||||
pub mod identifier;
|
||||
pub mod imports;
|
||||
mod int;
|
||||
pub mod node;
|
||||
mod nodes;
|
||||
pub mod parenthesize;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#![allow(clippy::derive_partial_eq_without_eq)]
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
|
||||
use num_bigint::BigInt;
|
||||
|
||||
use crate::int;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
/// See also [mod](https://docs.python.org/3/library/ast.html#ast.mod)
|
||||
@@ -466,7 +466,7 @@ pub struct StmtImportFrom {
|
||||
pub range: TextRange,
|
||||
pub module: Option<Identifier>,
|
||||
pub names: Vec<Alias>,
|
||||
pub level: Option<Int>,
|
||||
pub level: Option<u32>,
|
||||
}
|
||||
|
||||
impl From<StmtImportFrom> for Stmt {
|
||||
@@ -2578,42 +2578,13 @@ impl Ranged for Identifier {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Int(u32);
|
||||
|
||||
impl Int {
|
||||
pub fn new(i: u32) -> Self {
|
||||
Self(i)
|
||||
}
|
||||
pub fn to_u32(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
pub fn to_usize(&self) -> usize {
|
||||
self.0 as _
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::PartialEq<u32> for Int {
|
||||
#[inline]
|
||||
fn eq(&self, other: &u32) -> bool {
|
||||
self.0 == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::PartialEq<usize> for Int {
|
||||
#[inline]
|
||||
fn eq(&self, other: &usize) -> bool {
|
||||
self.0 as usize == *other
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, is_macro::Is)]
|
||||
pub enum Constant {
|
||||
None,
|
||||
Bool(bool),
|
||||
Str(StringConstant),
|
||||
Bytes(BytesConstant),
|
||||
Int(BigInt),
|
||||
Int(int::Int),
|
||||
Float(f64),
|
||||
Complex { real: f64, imag: f64 },
|
||||
Ellipsis,
|
||||
|
||||
@@ -577,7 +577,9 @@ impl<'a> Generator<'a> {
|
||||
statement!({
|
||||
self.p("from ");
|
||||
if let Some(level) = level {
|
||||
self.p(&".".repeat(level.to_usize()));
|
||||
for _ in 0..*level {
|
||||
self.p(".");
|
||||
}
|
||||
}
|
||||
if let Some(module) = module {
|
||||
self.p_id(module);
|
||||
|
||||
@@ -30,6 +30,7 @@ memchr = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
smallvec = { workspace = true }
|
||||
static_assertions = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -52,4 +53,5 @@ required-features = ["serde"]
|
||||
|
||||
[features]
|
||||
serde = ["dep:serde", "ruff_formatter/serde", "ruff_source_file/serde", "ruff_python_ast/serde"]
|
||||
default = ["serde"]
|
||||
schemars = ["dep:schemars", "ruff_formatter/schemars"]
|
||||
default = []
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
[
|
||||
{
|
||||
"indent_style": "Space",
|
||||
"indent_style": "space",
|
||||
"indent_width": 4
|
||||
},
|
||||
{
|
||||
"indent_style": "Space",
|
||||
"indent_style": "space",
|
||||
"indent_width": 2
|
||||
},
|
||||
{
|
||||
"indent_style": "Tab",
|
||||
"indent_style": "tab",
|
||||
"indent_width": 8
|
||||
},
|
||||
{
|
||||
"indent_style": "Tab",
|
||||
"indent_style": "tab",
|
||||
"indent_width": 4
|
||||
}
|
||||
]
|
||||
|
||||
@@ -366,7 +366,7 @@ if (
|
||||
):
|
||||
pass
|
||||
|
||||
z = (
|
||||
z = (
|
||||
a
|
||||
+
|
||||
# a: extracts this comment
|
||||
@@ -377,7 +377,7 @@ if (
|
||||
x and y
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
z = (
|
||||
(
|
||||
|
||||
@@ -52,3 +52,20 @@ a = (
|
||||
aaaaaaaaaaaaaaaaaaaaa = (
|
||||
o for o in self.registry.values if o.__class__ is not ModelAdmin
|
||||
)
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7623
|
||||
tuple(
|
||||
0 # comment
|
||||
for x in y
|
||||
)
|
||||
|
||||
tuple(
|
||||
(0 # comment
|
||||
for x in y)
|
||||
)
|
||||
|
||||
tuple(
|
||||
( # comment
|
||||
0 for x in y
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[
|
||||
{
|
||||
"indent_style": "Space",
|
||||
"indent_style": "space",
|
||||
"indent_width": 4
|
||||
},
|
||||
{
|
||||
"indent_style": "Space",
|
||||
"indent_style": "space",
|
||||
"indent_width": 2
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[
|
||||
{
|
||||
"indent_style": "Space",
|
||||
"indent_style": "space",
|
||||
"indent_width": 4
|
||||
},
|
||||
{
|
||||
"indent_style": "Space",
|
||||
"indent_style": "space",
|
||||
"indent_width": 1
|
||||
},
|
||||
{
|
||||
"indent_style": "Tab"
|
||||
"indent_style": "tab"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[
|
||||
{
|
||||
"indent_style": "Space",
|
||||
"indent_style": "space",
|
||||
"indent_width": 4
|
||||
},
|
||||
{
|
||||
"indent_style": "Space",
|
||||
"indent_style": "space",
|
||||
"indent_width": 2
|
||||
},
|
||||
{
|
||||
"indent_style": "Tab"
|
||||
"indent_style": "tab"
|
||||
}
|
||||
]
|
||||
|
||||
6
crates/ruff_python_formatter/resources/test/fixtures/ruff/form_feed.py
vendored
Normal file
6
crates/ruff_python_formatter/resources/test/fixtures/ruff/form_feed.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7624
|
||||
if symbol is not None:
|
||||
request["market"] = market["id"]
|
||||
# "remaining_volume": "0.0",
|
||||
else:
|
||||
pass
|
||||
161
crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.pyi
vendored
Normal file
161
crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.pyi
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
###
|
||||
# Blank lines around functions
|
||||
###
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
if True:
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
# comment
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
###
|
||||
# Blank lines around imports.
|
||||
###
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x # comment
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f(): pass # comment
|
||||
# comment
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
x = 1
|
||||
@@ -376,3 +376,11 @@ def f( # first
|
||||
def this_is_unusual() -> (please := no): ...
|
||||
|
||||
def this_is_unusual(x) -> (please := no): ...
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7465
|
||||
try:
|
||||
def test():
|
||||
pass
|
||||
#comment
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -104,6 +104,185 @@ if True: print("a") # 1
|
||||
elif True: print("b") # 2
|
||||
else: print("c") # 3
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7465
|
||||
if True:
|
||||
pass
|
||||
|
||||
# comment
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
|
||||
# comment
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
# comment
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
# comment
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass # comment
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
|
||||
# comment
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
if True:
|
||||
pass
|
||||
# comment
|
||||
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
if True:
|
||||
pass
|
||||
# comment
|
||||
|
||||
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
# comment
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
|
||||
# comment
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
pass
|
||||
|
||||
|
||||
# comment
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
if True:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
# a
|
||||
|
||||
# b
|
||||
# c
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
if True:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
# b
|
||||
# c
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7602
|
||||
if True:
|
||||
if True:
|
||||
if True:
|
||||
pass
|
||||
|
||||
#a
|
||||
#b
|
||||
#c
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
if True:
|
||||
if True:
|
||||
pass
|
||||
# b
|
||||
|
||||
# a
|
||||
# c
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
# Same indent
|
||||
|
||||
if True:
|
||||
if True:
|
||||
if True:
|
||||
pass
|
||||
|
||||
#a
|
||||
#b
|
||||
#c
|
||||
else:
|
||||
pass
|
||||
|
||||
if True:
|
||||
if True:
|
||||
if True:
|
||||
pass
|
||||
|
||||
# a
|
||||
# b
|
||||
# c
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/5337
|
||||
if parent_body:
|
||||
if current_body:
|
||||
|
||||
@@ -56,3 +56,23 @@ def func():
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7604
|
||||
import os
|
||||
|
||||
# Defaults for arguments are defined here
|
||||
# args.threshold = None;
|
||||
|
||||
|
||||
logger = logging.getLogger("FastProject")
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7604
|
||||
import os
|
||||
# comment
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
# comment
|
||||
x = 1
|
||||
|
||||
|
||||
@@ -2,17 +2,16 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{format_err, Context, Result};
|
||||
use clap::{command, Parser, ValueEnum};
|
||||
|
||||
use ruff_formatter::SourceCode;
|
||||
use ruff_python_index::CommentRangesBuilder;
|
||||
use ruff_python_parser::lexer::lex;
|
||||
use ruff_python_parser::{parse_tokens, Mode};
|
||||
use ruff_python_index::tokens_and_ranges;
|
||||
use ruff_python_parser::{parse_ok_tokens, Mode};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::collect_comments;
|
||||
use crate::{format_node, PyFormatOptions};
|
||||
use crate::{format_module_ast, PyFormatOptions};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug)]
|
||||
pub enum Emit {
|
||||
@@ -39,36 +38,25 @@ pub struct Cli {
|
||||
pub print_comments: bool,
|
||||
}
|
||||
|
||||
pub fn format_and_debug_print(input: &str, cli: &Cli, source_type: &Path) -> Result<String> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut comment_ranges = CommentRangesBuilder::default();
|
||||
|
||||
for result in lex(input, Mode::Module) {
|
||||
let (token, range) = match result {
|
||||
Ok((token, range)) => (token, range),
|
||||
Err(err) => bail!("Source contains syntax errors {err:?}"),
|
||||
};
|
||||
|
||||
comment_ranges.visit_token(&token, range);
|
||||
tokens.push(Ok((token, range)));
|
||||
}
|
||||
|
||||
let comment_ranges = comment_ranges.finish();
|
||||
pub fn format_and_debug_print(source: &str, cli: &Cli, source_type: &Path) -> Result<String> {
|
||||
let (tokens, comment_ranges) = tokens_and_ranges(source)
|
||||
.map_err(|err| format_err!("Source contains syntax errors {err:?}"))?;
|
||||
|
||||
// Parse the AST.
|
||||
let python_ast =
|
||||
parse_tokens(tokens, Mode::Module, "<filename>").context("Syntax error in input")?;
|
||||
let module =
|
||||
parse_ok_tokens(tokens, Mode::Module, "<filename>").context("Syntax error in input")?;
|
||||
|
||||
let options = PyFormatOptions::from_extension(source_type);
|
||||
let formatted = format_node(&python_ast, &comment_ranges, input, options)
|
||||
|
||||
let source_code = SourceCode::new(source);
|
||||
let formatted = format_module_ast(&module, &comment_ranges, source, options)
|
||||
.context("Failed to format node")?;
|
||||
if cli.print_ir {
|
||||
println!("{}", formatted.document().display(SourceCode::new(input)));
|
||||
println!("{}", formatted.document().display(source_code));
|
||||
}
|
||||
if cli.print_comments {
|
||||
// Print preceding, following and enclosing nodes
|
||||
let source_code = SourceCode::new(input);
|
||||
let decorated_comments = collect_comments(&python_ast, source_code, &comment_ranges);
|
||||
let decorated_comments = collect_comments(&module, source_code, &comment_ranges);
|
||||
if !decorated_comments.is_empty() {
|
||||
println!("# Comment decoration: Range, Preceding, Following, Enclosing, Comment");
|
||||
}
|
||||
@@ -86,13 +74,10 @@ pub fn format_and_debug_print(input: &str, cli: &Cli, source_type: &Path) -> Res
|
||||
comment.enclosing_node().kind(),
|
||||
comment.enclosing_node().range()
|
||||
),
|
||||
comment.slice().text(SourceCode::new(input)),
|
||||
comment.slice().text(source_code),
|
||||
);
|
||||
}
|
||||
println!(
|
||||
"{:#?}",
|
||||
formatted.context().comments().debug(SourceCode::new(input))
|
||||
);
|
||||
println!("{:#?}", formatted.context().comments().debug(source_code));
|
||||
}
|
||||
Ok(formatted
|
||||
.print()
|
||||
|
||||
@@ -86,17 +86,42 @@ impl Format<PyFormatContext<'_>> for FormatLeadingAlternateBranchComments<'_> {
|
||||
if let Some(first_leading) = self.comments.first() {
|
||||
// Leading comments only preserves the lines after the comment but not before.
|
||||
// Insert the necessary lines.
|
||||
if lines_before(first_leading.start(), f.context().source()) > 1 {
|
||||
write!(f, [empty_line()])?;
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
[empty_lines(lines_before(
|
||||
first_leading.start(),
|
||||
f.context().source()
|
||||
))]
|
||||
)?;
|
||||
|
||||
write!(f, [leading_comments(self.comments)])?;
|
||||
} else if let Some(last_preceding) = self.last_node {
|
||||
// The leading comments formatting ensures that it preserves the right amount of lines after
|
||||
// We need to take care of this ourselves, if there's no leading `else` comment.
|
||||
if lines_after_ignoring_trivia(last_preceding.end(), f.context().source()) > 1 {
|
||||
write!(f, [empty_line()])?;
|
||||
}
|
||||
// The leading comments formatting ensures that it preserves the right amount of lines
|
||||
// after We need to take care of this ourselves, if there's no leading `else` comment.
|
||||
// Since the `last_node` could be a compound node, we need to skip _all_ trivia.
|
||||
//
|
||||
// For example, here, when formatting the `if` statement, the `last_node` (the `while`)
|
||||
// would end at the end of `pass`, but we want to skip _all_ comments:
|
||||
// ```python
|
||||
// if True:
|
||||
// while True:
|
||||
// pass
|
||||
// # comment
|
||||
//
|
||||
// # comment
|
||||
// else:
|
||||
// ...
|
||||
// ```
|
||||
//
|
||||
// `lines_after_ignoring_trivia` is safe here, as we _know_ that the `else` doesn't
|
||||
// have any leading comments.
|
||||
write!(
|
||||
f,
|
||||
[empty_lines(lines_after_ignoring_trivia(
|
||||
last_preceding.end(),
|
||||
f.context().source()
|
||||
))]
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -299,7 +324,14 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLines {
|
||||
NodeLevel::TopLevel => match self.lines {
|
||||
0 | 1 => write!(f, [hard_line_break()]),
|
||||
2 => write!(f, [empty_line()]),
|
||||
_ => write!(f, [empty_line(), empty_line()]),
|
||||
_ => match f.options().source_type() {
|
||||
PySourceType::Stub => {
|
||||
write!(f, [empty_line()])
|
||||
}
|
||||
PySourceType::Python | PySourceType::Ipynb => {
|
||||
write!(f, [empty_line(), empty_line()])
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
NodeLevel::CompoundStatement => match self.lines {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user