Compare commits

..

1 Commits

Author SHA1 Message Date
Charlie Marsh
c3311f300e Avoid extra String allocation during parsing 2024-01-07 10:35:17 -05:00
205 changed files with 1322 additions and 3877 deletions

103
Cargo.lock generated
View File

@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.79"
version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355"
[[package]]
name = "argfile"
@@ -312,9 +312,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.13"
version = "4.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642"
checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d"
dependencies = [
"clap_builder",
"clap_derive",
@@ -382,7 +382,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -600,7 +600,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -611,7 +611,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -748,12 +748,23 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.8"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
"windows-sys 0.52.0",
]
[[package]]
@@ -1096,7 +1107,7 @@ dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -1239,9 +1250,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.152"
version = "0.2.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
[[package]]
name = "libcst"
@@ -1286,9 +1297,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.12"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
[[package]]
name = "lock_api"
@@ -1685,7 +1696,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -1770,9 +1781,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.76"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
checksum = "2dd5e8a1f1029c43224ad5898e50140c2aebb1705f19e67c918ebf5b9e797fe1"
dependencies = [
"unicode-ident",
]
@@ -1816,9 +1827,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.35"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
@@ -1985,7 +1996,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -2237,7 +2248,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -2528,15 +2539,15 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.28"
version = "0.38.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
dependencies = [
"bitflags 2.4.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -2642,9 +2653,9 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "serde"
version = "1.0.195"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
dependencies = [
"serde_derive",
]
@@ -2662,13 +2673,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.195"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -2730,7 +2741,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -2834,7 +2845,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -2850,9 +2861,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.48"
version = "2.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e"
dependencies = [
"proc-macro2",
"quote",
@@ -2861,15 +2872,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.9.0"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.4.1",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -2930,7 +2941,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -2942,7 +2953,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
"test-case-core",
]
@@ -2963,7 +2974,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -3100,7 +3111,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -3318,7 +3329,7 @@ checksum = "f49e7f3f3db8040a100710a11932239fd30697115e2ba4107080d8252939845e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -3412,7 +3423,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
"wasm-bindgen-shared",
]
@@ -3446,7 +3457,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3479,7 +3490,7 @@ checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]
[[package]]
@@ -3806,5 +3817,5 @@ checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.40",
]

View File

@@ -14,14 +14,14 @@ license = "MIT"
[workspace.dependencies]
aho-corasick = { version = "1.1.2" }
annotate-snippets = { version = "0.9.2", features = ["color"] }
anyhow = { version = "1.0.79" }
anyhow = { version = "1.0.76" }
argfile = { version = "0.1.6" }
assert_cmd = { version = "2.0.8" }
bincode = { version = "1.3.3" }
bitflags = { version = "2.4.1" }
cachedir = { version = "0.3.1" }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
clap = { version = "4.4.13", features = ["derive"] }
clap = { version = "4.4.12", features = ["derive"] }
clap_complete_command = { version = "0.5.1" }
clearscreen = { version = "2.0.0" }
codspeed-criterion-compat = { version = "2.3.3", default-features = false }
@@ -76,7 +76,7 @@ rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.16" }
seahash = { version ="4.1.0"}
semver = { version = "1.0.20" }
serde = { version = "1.0.195", features = ["derive"] }
serde = { version = "1.0.193", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.3" }
serde_json = { version = "1.0.109" }
serde_test = { version = "1.0.152" }
@@ -89,7 +89,7 @@ static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
syn = { version = "2.0.40" }
tempfile = { version ="3.9.0"}
tempfile = { version ="3.8.1"}
test-case = { version = "3.3.1" }
thiserror = { version = "1.0.51" }
tikv-jemallocator = { version ="0.5.0"}

View File

@@ -418,7 +418,6 @@ Ruff is used by a number of major open-source projects and companies, including:
- Netflix ([Dispatch](https://github.com/Netflix/dispatch))
- [Neon](https://github.com/neondatabase/neon)
- [NoneBot](https://github.com/nonebot/nonebot2)
- [NumPyro](https://github.com/pyro-ppl/numpyro)
- [ONNX](https://github.com/onnx/onnx)
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
- [PDM](https://github.com/pdm-project/pdm)
@@ -430,7 +429,6 @@ Ruff is used by a number of major open-source projects and companies, including:
- [PostHog](https://github.com/PostHog/posthog)
- Prefect ([Python SDK](https://github.com/PrefectHQ/prefect), [Marvin](https://github.com/PrefectHQ/marvin))
- [PyInstaller](https://github.com/pyinstaller/pyinstaller)
- [PyMC](https://github.com/pymc-devs/pymc/)
- [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing)
- [PyTorch](https://github.com/pytorch/pytorch)
- [Pydantic](https://github.com/pydantic/pydantic)

View File

@@ -55,7 +55,7 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
&case,
|b, case| {
// Tokenize the source.
let tokens: Vec<_> = lexer::lex(case.code(), Mode::Module).collect();
let tokens = lexer::lex(case.code(), Mode::Module).collect::<Vec<_>>();
// Parse the source.
let ast = parse_program_tokens(tokens.clone(), case.code(), false).unwrap();

View File

@@ -25,9 +25,10 @@ use ruff_notebook::NotebookIndex;
use ruff_python_ast::imports::ImportMap;
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::resolver::Resolver;
use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy, Resolver};
use ruff_workspace::Settings;
use crate::cache;
use crate::diagnostics::Diagnostics;
/// [`Path`] that is relative to the package root in [`PackageCache`].
@@ -85,7 +86,6 @@ pub(crate) struct Cache {
changes: Mutex<Vec<Change>>,
/// The "current" timestamp used as cache for the updates of
/// [`FileCache::last_seen`]
#[allow(clippy::struct_field_names)]
last_seen_cache: u64,
}
@@ -442,7 +442,7 @@ pub(super) struct CacheMessage {
pub(crate) trait PackageCaches {
fn get(&self, package_root: &Path) -> Option<&Cache>;
fn persist(self) -> Result<()>;
fn persist(self) -> anyhow::Result<()>;
}
impl<T> PackageCaches for Option<T>
@@ -468,17 +468,27 @@ pub(crate) struct PackageCacheMap<'a>(FxHashMap<&'a Path, Cache>);
impl<'a> PackageCacheMap<'a> {
pub(crate) fn init(
pyproject_config: &PyprojectConfig,
package_roots: &FxHashMap<&'a Path, Option<&'a Path>>,
resolver: &Resolver,
) -> Self {
fn init_cache(path: &Path) {
if let Err(e) = init(path) {
if let Err(e) = cache::init(path) {
error!("Failed to initialize cache at {}: {e:?}", path.display());
}
}
for settings in resolver.settings() {
init_cache(&settings.cache_dir);
match pyproject_config.strategy {
PyprojectDiscoveryStrategy::Fixed => {
init_cache(&pyproject_config.settings.cache_dir);
}
PyprojectDiscoveryStrategy::Hierarchical => {
for settings in
std::iter::once(&pyproject_config.settings).chain(resolver.settings())
{
init_cache(&settings.cache_dir);
}
}
}
Self(
@@ -488,7 +498,7 @@ impl<'a> PackageCacheMap<'a> {
.unique()
.par_bridge()
.map(|cache_root| {
let settings = resolver.resolve(cache_root);
let settings = resolver.resolve(cache_root, pyproject_config);
let cache = Cache::open(cache_root.to_path_buf(), settings);
(cache_root, cache)
})

View File

@@ -38,6 +38,7 @@ pub(crate) fn add_noqa(
.flatten()
.map(ResolvedFile::path)
.collect::<Vec<_>>(),
pyproject_config,
);
let start = Instant::now();
@@ -56,7 +57,7 @@ pub(crate) fn add_noqa(
.parent()
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);
let settings = resolver.resolve(path);
let settings = resolver.resolve(path, pyproject_config);
let source_kind = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind,
Ok(None) => return None,

View File

@@ -57,11 +57,16 @@ pub(crate) fn check(
.flatten()
.map(ResolvedFile::path)
.collect::<Vec<_>>(),
pyproject_config,
);
// Load the caches.
let caches = if bool::from(cache) {
Some(PackageCacheMap::init(&package_roots, &resolver))
Some(PackageCacheMap::init(
pyproject_config,
&package_roots,
&resolver,
))
} else {
None
};
@@ -76,7 +81,7 @@ pub(crate) fn check(
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);
let settings = resolver.resolve(path);
let settings = resolver.resolve(path, pyproject_config);
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
&& match_exclusion(
@@ -123,7 +128,7 @@ pub(crate) fn check(
Some(result.unwrap_or_else(|(path, message)| {
if let Some(path) = &path {
let settings = resolver.resolve(path);
let settings = resolver.resolve(path, pyproject_config);
if settings.linter.rules.enabled(Rule::IOError) {
let dummy =
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();

View File

@@ -4,7 +4,7 @@ use anyhow::Result;
use ruff_linter::packaging;
use ruff_linter::settings::flags;
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig, Resolver};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig};
use crate::args::CliOverrides;
use crate::diagnostics::{lint_stdin, Diagnostics};
@@ -18,20 +18,20 @@ pub(crate) fn check_stdin(
noqa: flags::Noqa,
fix_mode: flags::FixMode,
) -> Result<Diagnostics> {
let mut resolver = Resolver::new(pyproject_config);
if resolver.force_exclude() {
if pyproject_config.settings.file_resolver.force_exclude {
if let Some(filename) = filename {
if !python_file_at_path(filename, &mut resolver, overrides)? {
if !python_file_at_path(filename, pyproject_config, overrides)? {
if fix_mode.is_apply() {
parrot_stdin()?;
}
return Ok(Diagnostics::default());
}
if filename.file_name().is_some_and(|name| {
match_exclusion(filename, name, &resolver.base_settings().linter.exclude)
}) {
let lint_settings = &pyproject_config.settings.linter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude))
{
if fix_mode.is_apply() {
parrot_stdin()?;
}
@@ -41,13 +41,13 @@ pub(crate) fn check_stdin(
}
let stdin = read_from_stdin()?;
let package_root = filename.and_then(Path::parent).and_then(|path| {
packaging::detect_package_root(path, &resolver.base_settings().linter.namespace_packages)
packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages)
});
let mut diagnostics = lint_stdin(
filename,
package_root,
stdin,
resolver.base_settings(),
&pyproject_config.settings,
noqa,
fix_mode,
)?;

View File

@@ -25,7 +25,9 @@ use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver};
use ruff_workspace::resolver::{
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, Resolver,
};
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
@@ -77,7 +79,7 @@ pub(crate) fn format(
return Ok(ExitStatus::Success);
}
warn_incompatible_formatter_settings(&resolver);
warn_incompatible_formatter_settings(&pyproject_config, Some(&resolver));
// Discover the package root for each Python file.
let package_roots = resolver.package_roots(
@@ -86,6 +88,7 @@ pub(crate) fn format(
.flatten()
.map(ResolvedFile::path)
.collect::<Vec<_>>(),
&pyproject_config,
);
let caches = if cli.no_cache {
@@ -96,7 +99,11 @@ pub(crate) fn format(
#[cfg(debug_assertions)]
crate::warn_user!("Detected debug build without --no-cache.");
Some(PackageCacheMap::init(&package_roots, &resolver))
Some(PackageCacheMap::init(
&pyproject_config,
&package_roots,
&resolver,
))
};
let start = Instant::now();
@@ -111,7 +118,7 @@ pub(crate) fn format(
return None;
};
let settings = resolver.resolve(path);
let settings = resolver.resolve(path, &pyproject_config);
// Ignore files that are excluded from formatting
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
@@ -716,10 +723,15 @@ impl Display for FormatCommandError {
}
}
pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
pub(super) fn warn_incompatible_formatter_settings(
pyproject_config: &PyprojectConfig,
resolver: Option<&Resolver>,
) {
// First, collect all rules that are incompatible regardless of the linter-specific settings.
let mut incompatible_rules = FxHashSet::default();
for setting in resolver.settings() {
for setting in std::iter::once(&pyproject_config.settings)
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
{
for rule in [
// The formatter might collapse implicit string concatenation on a single line.
Rule::SingleLineImplicitStringConcatenation,
@@ -748,7 +760,9 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
}
// Next, validate settings-specific incompatibilities.
for setting in resolver.settings() {
for setting in std::iter::once(&pyproject_config.settings)
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
{
// Validate all rules that rely on tab styles.
if setting.linter.rules.enabled(Rule::TabIndentation)
&& setting.formatter.indent_style.is_tab()

View File

@@ -6,7 +6,7 @@ use log::error;
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path};
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
@@ -27,23 +27,24 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
cli.stdin_filename.as_deref(),
)?;
let mut resolver = Resolver::new(&pyproject_config);
warn_incompatible_formatter_settings(&resolver);
warn_incompatible_formatter_settings(&pyproject_config, None);
let mode = FormatMode::from_cli(cli);
if resolver.force_exclude() {
if pyproject_config.settings.file_resolver.force_exclude {
if let Some(filename) = cli.stdin_filename.as_deref() {
if !python_file_at_path(filename, &mut resolver, overrides)? {
if !python_file_at_path(filename, &pyproject_config, overrides)? {
if mode.is_write() {
parrot_stdin()?;
}
return Ok(ExitStatus::Success);
}
if filename.file_name().is_some_and(|name| {
match_exclusion(filename, name, &resolver.base_settings().formatter.exclude)
}) {
let format_settings = &pyproject_config.settings.formatter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude))
{
if mode.is_write() {
parrot_stdin()?;
}
@@ -62,7 +63,12 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
};
// Format the file.
match format_source_code(path, &resolver.base_settings().formatter, source_type, mode) {
match format_source_code(
path,
&pyproject_config.settings.formatter,
source_type,
mode,
) {
Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check | FormatMode::Diff => {

View File

@@ -18,7 +18,6 @@ struct Explanation<'a> {
summary: &'a str,
message_formats: &'a [&'a str],
fix: String,
#[allow(clippy::struct_field_names)]
explanation: Option<&'a str>,
preview: bool,
}

View File

@@ -29,7 +29,7 @@ pub(crate) fn show_settings(
bail!("No files found under the given path");
};
let settings = resolver.resolve(&path);
let settings = resolver.resolve(&path, pyproject_config);
writeln!(writer, "Resolved settings for: {path:?}")?;
if let Some(settings_path) = pyproject_config.path.as_ref() {

View File

@@ -27,7 +27,7 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
use ruff_cli::args::{CliOverrides, FormatArguments, FormatCommand, LogLevelArgs};
use ruff_cli::args::{FormatCommand, LogLevelArgs};
use ruff_cli::resolve::resolve;
use ruff_formatter::{FormatError, LineWidth, PrintError};
use ruff_linter::logging::LogLevel;
@@ -38,24 +38,24 @@ use ruff_python_formatter::{
use ruff_python_parser::ParseError;
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, CliOverrides)> {
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
#[allow(clippy::type_complexity)]
fn ruff_check_paths(
dirs: &[PathBuf],
) -> anyhow::Result<(
Vec<Result<ResolvedFile, ignore::Error>>,
Resolver,
PyprojectConfig,
)> {
let args_matches = FormatCommand::command()
.no_binary_name(true)
.get_matches_from(dirs);
let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?;
let (cli, overrides) = arguments.partition();
Ok((cli, overrides))
}
/// Find the [`PyprojectConfig`] to use for formatting.
fn find_pyproject_config(
cli: &FormatArguments,
overrides: &CliOverrides,
) -> anyhow::Result<PyprojectConfig> {
let mut pyproject_config = resolve(
cli.isolated,
cli.config.as_deref(),
overrides,
&overrides,
cli.stdin_filename.as_deref(),
)?;
// We don't want to format pyproject.toml
@@ -64,18 +64,11 @@ fn find_pyproject_config(
FilePattern::Builtin("*.pyi"),
])
.unwrap();
Ok(pyproject_config)
}
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
#[allow(clippy::type_complexity)]
fn ruff_check_paths<'a>(
pyproject_config: &'a PyprojectConfig,
cli: &FormatArguments,
overrides: &CliOverrides,
) -> anyhow::Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver<'a>)> {
let (paths, resolver) = python_files_in_path(&cli.files, pyproject_config, overrides)?;
Ok((paths, resolver))
let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?;
if paths.is_empty() {
bail!("no python files in {:?}", dirs)
}
Ok((paths, resolver, pyproject_config))
}
/// Collects statistics over the formatted files to compute the Jaccard index or the similarity
@@ -223,7 +216,6 @@ pub(crate) struct Args {
#[arg(long)]
pub(crate) files_with_errors: Option<u32>,
#[clap(flatten)]
#[allow(clippy::struct_field_names)]
pub(crate) log_level_args: LogLevelArgs,
}
@@ -459,17 +451,11 @@ fn format_dev_project(
files[0].display()
);
// TODO(konstin): Respect black's excludes.
// TODO(konstin): black excludes
// Find files to check (or in this case, format twice). Adapted from ruff_cli
// First argument is ignored
let (cli, overrides) = parse_cli(files)?;
let pyproject_config = find_pyproject_config(&cli, &overrides)?;
let (paths, resolver) = ruff_check_paths(&pyproject_config, &cli, &overrides)?;
if paths.is_empty() {
bail!("No Python files found under the given path(s)");
}
let (paths, resolver, pyproject_config) = ruff_check_paths(files)?;
let results = {
let pb_span =
@@ -482,7 +468,14 @@ fn format_dev_project(
#[cfg(feature = "singlethreaded")]
let iter = { paths.into_iter() };
iter.map(|path| {
let result = format_dir_entry(path, stability_check, write, &black_options, &resolver);
let result = format_dir_entry(
path,
stability_check,
write,
&black_options,
&resolver,
&pyproject_config,
);
pb_span.pb_inc(1);
result
})
@@ -532,13 +525,14 @@ fn format_dev_project(
})
}
/// Error handling in between walkdir and `format_dev_file`.
/// Error handling in between walkdir and `format_dev_file`
fn format_dir_entry(
resolved_file: Result<ResolvedFile, ignore::Error>,
stability_check: bool,
write: bool,
options: &BlackOptions,
resolver: &Resolver,
pyproject_config: &PyprojectConfig,
) -> anyhow::Result<(Result<Statistics, CheckFileError>, PathBuf), Error> {
let resolved_file = resolved_file.context("Iterating the files in the repository failed")?;
// For some reason it does not filter in the beginning
@@ -549,7 +543,7 @@ fn format_dir_entry(
let path = resolved_file.into_path();
let mut options = options.to_py_format_options(&path);
let settings = resolver.resolve(&path);
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
if settings.formatter.line_width != LineWidth::default() {
options = options.with_line_width(settings.formatter.line_width);

View File

@@ -1472,11 +1472,6 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
}
fn fits_text(&mut self, text: Text, args: PrintElementArgs) -> Fits {
fn exceeds_width(fits: &FitsMeasurer, args: PrintElementArgs) -> bool {
fits.state.line_width > fits.options().line_width.into()
&& !args.measure_mode().allows_text_overflow()
}
let indent = std::mem::take(&mut self.state.pending_indent);
self.state.line_width +=
u32::from(indent.level()) * self.options().indent_width() + u32::from(indent.align());
@@ -1498,13 +1493,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
return Fits::No;
}
match args.measure_mode() {
MeasureMode::FirstLine => {
return if exceeds_width(self, args) {
Fits::No
} else {
Fits::Yes
};
}
MeasureMode::FirstLine => return Fits::Yes,
MeasureMode::AllLines
| MeasureMode::AllLinesAllowTextOverflow => {
self.state.line_width = 0;
@@ -1522,7 +1511,9 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
}
}
if exceeds_width(self, args) {
if self.state.line_width > self.options().line_width.into()
&& !args.measure_mode().allows_text_overflow()
{
return Fits::No;
}

View File

@@ -71,8 +71,6 @@ foo.is_(True)
bar.is_not(False)
next(iter([]), False)
sa.func.coalesce(tbl.c.valid, False)
setVisible(True)
set_visible(True)
class Registry:
@@ -116,6 +114,3 @@ from typing import override
@override
def func(x: bool):
pass
settings(True)

View File

@@ -8,14 +8,46 @@ class MyClass:
self.id = 10
self.dir = "."
def int(self):
pass
def str(self):
pass
def method_usage(self) -> str:
from typing import TypedDict
class MyClass(TypedDict):
id: int
from threading import Event
class CustomEvent(Event):
def set(self) -> None:
...
def str(self) -> None:
...
from logging import Filter, LogRecord
class CustomFilter(Filter):
def filter(self, record: LogRecord) -> bool:
...
def str(self) -> None:
...
from typing_extensions import override
class MyClass:
@override
def str(self):
pass
def attribute_usage(self) -> id:
def int(self):
pass

View File

@@ -1,9 +1,3 @@
import warnings
import typing_extensions
from collections.abc import Callable
from warnings import deprecated
def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None:
...
@@ -51,24 +45,3 @@ class Demo:
def func() -> None:
"""Docstrings are excluded from this rule. Some padding."""
@warnings.deprecated("Veeeeeeeeeeeeeeeeeeeeeeery long deprecation message, but that's okay")
def deprecated_function() -> None: ...
@typing_extensions.deprecated("Another loooooooooooooooooooooong deprecation message, it's still okay")
def another_deprecated_function() -> None: ...
@deprecated("A third loooooooooooooooooooooooooooooong deprecation message")
def a_third_deprecated_function() -> None: ...
def not_warnings_dot_deprecated(
msg: str
) -> Callable[[Callable[[], None]], Callable[[], None]]: ...
@not_warnings_dot_deprecated("Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!")
def not_a_deprecated_function() -> None: ...

View File

@@ -1,7 +1,3 @@
import warnings
import typing_extensions
from typing_extensions import deprecated
def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
def f2(
x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
@@ -42,25 +38,3 @@ class Demo:
def func() -> None:
"""Docstrings are excluded from this rule. Some padding.""" # OK
@warnings.deprecated(
"Veeeeeeeeeeeeeeeeeeeeeeery long deprecation message, but that's okay" # OK
)
def deprecated_function() -> None: ...
@typing_extensions.deprecated(
"Another loooooooooooooooooooooong deprecation message, it's still okay" # OK
)
def another_deprecated_function() -> None: ...
@deprecated("A third loooooooooooooooooooooooooooooong deprecation message") # OK
def a_third_deprecated_function() -> None: ...
def not_warnings_dot_deprecated(
msg: str
) -> Callable[[Callable[[], None]], Callable[[], None]]: ...
@not_warnings_dot_deprecated(
"Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053
)
def not_a_deprecated_function() -> None: ...

View File

@@ -1,23 +0,0 @@
def foo(d: dict[str, str]) -> None:
for k, v in zip(d.keys(), d.values()): # SIM911
...
for k, v in zip(d.keys(), d.values(), strict=True): # SIM911
...
for k, v in zip(d.keys(), d.values(), struct=True): # OK
...
d1 = d2 = {}
for k, v in zip(d1.keys(), d2.values()): # OK
...
for k, v in zip(d1.items(), d2.values()): # OK
...
for k, v in zip(d2.keys(), d2.values()): # SIM911
...
items = zip(x.keys(), x.values()) # OK

View File

@@ -150,21 +150,3 @@ class Test:
Args:
arg1: some description of arg
"""
def select_data(
query: str,
args: tuple,
database: str,
auto_save: bool,
) -> None:
"""This function has an argument `args`, which shouldn't be mistaken for a section.
Args:
query:
Query template.
args:
A list of arguments.
database:
Which database to connect to ("origin" or "destination").
"""

View File

@@ -536,29 +536,9 @@ def non_empty_blank_line_before_section(): # noqa: D416
"""Toggle the gizmo.
The function's description.
Returns
-------
A value of some sort.
"""
def lowercase_sub_section_header():
"""Below, `returns:` should _not_ be considered a section header.
Args:
Here's a note.
returns:
"""
def titlecase_sub_section_header():
"""Below, `Returns:` should be considered a section header.
Args:
Here's a note.
Returns:
"""

View File

@@ -1,6 +0,0 @@
import os
x = 1
if x > 0:
import os

View File

@@ -91,12 +91,3 @@ from typing_extensions import dataclass_transform
# UP035
from backports.strenum import StrEnum
# UP035
from typing_extensions import override
# UP035
from typing_extensions import Buffer
# UP035
from typing_extensions import get_original_bases

View File

@@ -1,5 +1,5 @@
import typing
from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
from typing import Annotated, Any, Literal, Optional, Tuple, Union
def f(arg: int):
@@ -257,13 +257,3 @@ from custom_typing import MaybeInt
def f(arg: MaybeInt = None):
pass
# Hashable
def f(arg: Hashable = None): # OK
pass
def f(arg: Hashable | int = None): # OK
pass

View File

@@ -1,114 +0,0 @@
# See https://docs.python.org/3/reference/expressions.html#operator-precedence
# for the official docs on operator precedence.
#
# Most importantly, `and` *always* takes precedence over `or`.
#
# `not` (the third boolean/logical operator) takes precedence over both,
# but the rule there is easier to remember,
# so we don't emit a diagnostic if a `not` expression is unparenthesized
# as part of a chain.
a, b, c = 1, 0, 2
x = a or b and c # RUF021: => `a or (b and c)`
x = a or b and c # looooooooooooooooooooooooooooooong comment but it won't prevent an autofix
a, b, c = 0, 1, 2
y = a and b or c # RUF021: => `(a and b) or c`
a, b, c, d = 1, 2, 0, 3
if a or b or c and d: # RUF021: => `a or b or (c and d)`
pass
a, b, c, d = 0, 0, 2, 3
if bool():
pass
elif a or b and c or d: # RUF021: => `a or (b and c) or d`
pass
a, b, c, d = 0, 1, 0, 2
while a and b or c and d: # RUF021: => `(and b) or (c and d)`
pass
b, c, d, e = 2, 3, 0, 4
# RUF021: => `a or b or c or (d and e)`:
z = [a for a in range(5) if a or b or c or d and e]
a, b, c, d = 0, 1, 3, 0
assert not a and b or c or d # RUF021: => `(not a and b) or c or d`
if (not a) and b or c or d: # RUF021: => `((not a) and b) or c or d`
if (not a and b) or c or d: # OK
pass
if (
some_reasonably_long_condition
or some_other_reasonably_long_condition
and some_third_reasonably_long_condition
or some_fourth_reasonably_long_condition
and some_fifth_reasonably_long_condition
# a commment
and some_sixth_reasonably_long_condition
and some_seventh_reasonably_long_condition
# another comment
or some_eighth_reasonably_long_condition
):
pass
#############################################
# If they're all the same operator, it's fine
#############################################
x = not a and c # OK
if a or b or c: # OK
pass
while a and b and c: # OK
pass
###########################################################
# We don't consider `not` as part of a chain as problematic
###########################################################
x = not a or not b or not c # OK
#####################################
# If they're parenthesized, it's fine
#####################################
a, b, c = 1, 0, 2
x = a or (b and c) # OK
x2 = (a or b) and c # OK
x3 = (a or b) or c # OK
x4 = (a and b) and c # OK
a, b, c = 0, 1, 2
y = (a and b) or c # OK
yy = a and (b or c) # OK
a, b, c, d = 1, 2, 0, 3
if a or b or (c and d): # OK
pass
a, b, c, d = 0, 0, 2, 3
if bool():
pass
elif a or (b and c) or d: # OK
pass
a, b, c, d = 0, 1, 0, 2
while (a and b) or (c and d): # OK
pass
b, c, d, e = 2, 3, 0, 4
z = [a for a in range(5) if a or b or c or (d and e)] # OK
a, b = 1, 2
if (not a) or b: # OK
if (not a) and b: # OK
pass
a, b, c, d = 0, 1, 3, 0
assert ((not a) and b) or c or d # OK

View File

@@ -1,14 +1,12 @@
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_diagnostics::Diagnostic;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Binding, BindingKind, Imported, ScopeKind};
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::fix;
use crate::rules::{
flake8_builtins, flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint,
ruff,
flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint, ruff,
};
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
@@ -28,7 +26,6 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::UndefinedLocal,
Rule::UnusedAnnotation,
Rule::UnusedClassMethodArgument,
Rule::BuiltinAttributeShadowing,
Rule::UnusedFunctionArgument,
Rule::UnusedImport,
Rule::UnusedLambdaArgument,
@@ -147,9 +144,9 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding
.source
.map_or(true, |right| !checker.semantic.same_branch(left, right))
binding.source.map_or(true, |right| {
checker.semantic.different_branches(left, right)
})
}) {
continue;
}
@@ -236,9 +233,9 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding
.source
.map_or(true, |right| !checker.semantic.same_branch(left, right))
binding.source.map_or(true, |right| {
checker.semantic.different_branches(left, right)
})
}) {
continue;
}
@@ -250,33 +247,9 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
},
binding.range(),
);
if let Some(range) = binding.parent_range(&checker.semantic) {
diagnostic.set_parent(range.start());
}
if checker.settings.preview.is_enabled() {
if let Some(import) = binding.as_any_import() {
if let Some(source) = binding.source {
diagnostic.try_set_fix(|| {
let statement = checker.semantic().statement(source);
let parent = checker.semantic().parent_statement(source);
let edit = fix::edits::remove_unused_imports(
std::iter::once(import.member_name().as_ref()),
statement,
parent,
checker.locator(),
checker.stylist(),
checker.indexer(),
)?;
Ok(Fix::safe_edit(edit).isolate(Checker::isolation(
checker.semantic().parent_statement_id(source),
)))
});
}
}
}
diagnostics.push(diagnostic);
}
}
@@ -299,18 +272,6 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
ruff::rules::asyncio_dangling_binding(scope, &checker.semantic, &mut diagnostics);
}
if let Some(class_def) = scope.kind.as_class() {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker,
scope_id,
scope,
class_def,
&mut diagnostics,
);
}
}
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
if checker.enabled(Rule::UnusedVariable) {
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);

View File

@@ -242,7 +242,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if !checker.semantic.current_scope().kind.is_class() {
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker, class_def, id, *range,
);
}
} else {
if checker.enabled(Rule::BuiltinVariableShadowing) {
flake8_builtins::rules::builtin_variable_shadowing(checker, id, *range);
}
@@ -718,7 +724,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
if checker.enabled(Rule::BooleanPositionalValueInCall) {
flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, call);
flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, args, func);
}
if checker.enabled(Rule::Debugger) {
flake8_debugger::rules::debugger_call(checker, expr, func);
@@ -863,9 +869,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::DictGetWithNoneDefault) {
flake8_simplify::rules::dict_get_with_none_default(checker, expr);
}
if checker.enabled(Rule::ZipDictKeysAndValues) {
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
}
if checker.any_enabled(&[
Rule::OsPathAbspath,
Rule::OsChmod,
@@ -1489,9 +1492,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryKeyCheck) {
ruff::rules::unnecessary_key_check(checker, expr);
}
if checker.enabled(Rule::ParenthesizeChainedOperators) {
ruff::rules::parenthesize_chained_logical_operators(checker, bool_op);
}
}
Expr::NamedExpr(..) => {
if checker.enabled(Rule::AssignmentInAssert) {

View File

@@ -347,7 +347,17 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::FStringDocstring) {
flake8_bugbear::rules::f_string_docstring(checker, body);
}
if !checker.semantic.current_scope().kind.is_class() {
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_method_shadowing(
checker,
class_def,
name,
decorator_list,
name.range(),
);
}
} else {
if checker.enabled(Rule::BuiltinVariableShadowing) {
flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range());
}

View File

@@ -472,7 +472,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Simplify, "300") => (RuleGroup::Stable, rules::flake8_simplify::rules::YodaConditions),
(Flake8Simplify, "401") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet),
(Flake8Simplify, "910") => (RuleGroup::Stable, rules::flake8_simplify::rules::DictGetWithNoneDefault),
(Flake8Simplify, "911") => (RuleGroup::Preview, rules::flake8_simplify::rules::ZipDictKeysAndValues),
// flake8-copyright
#[allow(deprecated)]
@@ -924,7 +923,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "018") => (RuleGroup::Preview, rules::ruff::rules::AssignmentInAssert),
(Ruff, "019") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryKeyCheck),
(Ruff, "020") => (RuleGroup::Preview, rules::ruff::rules::NeverUnion),
(Ruff, "021") => (RuleGroup::Preview, rules::ruff::rules::ParenthesizeChainedOperators),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),

View File

@@ -153,17 +153,13 @@ impl<'a> SectionContexts<'a> {
while let Some(line) = lines.next() {
if let Some(section_kind) = suspected_as_section(&line, style) {
let indent = leading_space(&line);
let indent_size = indent.text_len();
let section_name = leading_words(&line);
let section_name_size = section_name.text_len();
let section_name_range = TextRange::at(indent.text_len(), section_name.text_len());
if is_docstring_section(
&line,
indent_size,
section_name_size,
section_kind,
last.as_ref(),
section_name_range,
previous_line.as_ref(),
lines.peek(),
) {
@@ -174,8 +170,7 @@ impl<'a> SectionContexts<'a> {
last = Some(SectionContextData {
kind: section_kind,
indent_size: indent.text_len(),
name_range: TextRange::at(line.start() + indent_size, section_name_size),
name_range: section_name_range + line.start(),
range: TextRange::empty(line.start()),
summary_full_end: line.full_end(),
});
@@ -209,8 +204,8 @@ impl<'a> SectionContexts<'a> {
}
impl<'a> IntoIterator for &'a SectionContexts<'a> {
type Item = SectionContext<'a>;
type IntoIter = SectionContextsIter<'a>;
type Item = SectionContext<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
@@ -262,9 +257,6 @@ impl ExactSizeIterator for SectionContextsIter<'_> {}
struct SectionContextData {
kind: SectionKind,
/// The size of the indentation of the section name.
indent_size: TextSize,
/// Range of the section name, relative to the [`Docstring::body`]
name_range: TextRange,
@@ -409,15 +401,12 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind>
/// Check if the suspected context is really a section header.
fn is_docstring_section(
line: &Line,
indent_size: TextSize,
section_name_size: TextSize,
section_kind: SectionKind,
previous_section: Option<&SectionContextData>,
section_name_range: TextRange,
previous_line: Option<&Line>,
next_line: Option<&Line>,
) -> bool {
// Determine whether the current line looks like a section header, e.g., "Args:".
let section_name_suffix = line[usize::from(indent_size + section_name_size)..].trim();
let section_name_suffix = line[usize::from(section_name_range.end())..].trim();
let this_looks_like_a_section_name =
section_name_suffix == ":" || section_name_suffix.is_empty();
if !this_looks_like_a_section_name {
@@ -450,25 +439,5 @@ fn is_docstring_section(
return false;
}
// Determine if this is a sub-section within another section, like `args` in:
// ```python
// def func(args: tuple[int]):
// """Toggle the gizmo.
//
// Args:
// args: The arguments to the function.
// """
// ```
// However, if the header is an _exact_ match (like `Returns:`, as opposed to `returns:`), then
// continue to treat it as a section header.
if let Some(previous_section) = previous_section {
if previous_section.indent_size < indent_size {
let verbatim = &line[TextRange::at(indent_size, section_name_size)];
if section_kind.as_str() != verbatim {
return false;
}
}
}
true
}

View File

@@ -51,29 +51,13 @@ pub(super) fn is_allowed_func_def(name: &str) -> bool {
/// Returns `true` if an argument is allowed to use a boolean trap. To return
/// `true`, the function name must be explicitly allowed, and the argument must
/// be either the first or second argument in the call.
pub(super) fn allow_boolean_trap(call: &ast::ExprCall) -> bool {
let func_name = match call.func.as_ref() {
Expr::Attribute(ast::ExprAttribute { attr, .. }) => attr.as_str(),
Expr::Name(ast::ExprName { id, .. }) => id.as_str(),
_ => return false,
};
// If the function name is explicitly allowed, then the boolean trap is
// allowed.
if is_allowed_func_call(func_name) {
return true;
pub(super) fn allow_boolean_trap(func: &Expr) -> bool {
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func {
return is_allowed_func_call(attr);
}
// If the function appears to be a setter (e.g., `set_visible` or `setVisible`), then the
// boolean trap is allowed. We want to avoid raising a violation for cases in which the argument
// is positional-only and third-party, and this tends to be the case for setters.
if call.arguments.args.len() == 1 {
if func_name
.strip_prefix("set")
.is_some_and(|suffix| suffix.starts_with(|c: char| c == '_' || c.is_ascii_uppercase()))
{
return true;
}
if let Expr::Name(ast::ExprName { id, .. }) = func {
return is_allowed_func_call(id);
}
false

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::Expr;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -45,16 +45,11 @@ impl Violation for BooleanPositionalValueInCall {
}
}
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, call: &ast::ExprCall) {
if allow_boolean_trap(call) {
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, args: &[Expr], func: &Expr) {
if allow_boolean_trap(func) {
return;
}
for arg in call
.arguments
.args
.iter()
.filter(|arg| arg.is_boolean_literal_expr())
{
for arg in args.iter().filter(|arg| arg.is_boolean_literal_expr()) {
checker
.diagnostics
.push(Diagnostic::new(BooleanPositionalValueInCall, arg.range()));

View File

@@ -81,12 +81,12 @@ FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
21 | kwonly_nonvalued_nohint,
|
FBT.py:91:19: FBT001 Boolean-typed positional argument in function definition
FBT.py:89:19: FBT001 Boolean-typed positional argument in function definition
|
90 | # FBT001: Boolean positional arg in function definition
91 | def foo(self, value: bool) -> None:
88 | # FBT001: Boolean positional arg in function definition
89 | def foo(self, value: bool) -> None:
| ^^^^^ FBT001
92 | pass
90 | pass
|

View File

@@ -28,10 +28,14 @@ FBT.py:57:17: FBT003 Boolean positional value in function call
59 | mylist.index(True)
|
FBT.py:121:10: FBT003 Boolean positional value in function call
|
121 | settings(True)
| ^^^^ FBT003
|
FBT.py:69:38: FBT003 Boolean positional value in function call
|
67 | os.set_blocking(0, False)
68 | g_action.set_enabled(True)
69 | settings.set_enable_developer_extras(True)
| ^^^^ FBT003
70 | foo.is_(True)
71 | bar.is_not(False)
|

View File

@@ -81,26 +81,26 @@ FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
21 | kwonly_nonvalued_nohint,
|
FBT.py:91:19: FBT001 Boolean-typed positional argument in function definition
FBT.py:89:19: FBT001 Boolean-typed positional argument in function definition
|
90 | # FBT001: Boolean positional arg in function definition
91 | def foo(self, value: bool) -> None:
88 | # FBT001: Boolean positional arg in function definition
89 | def foo(self, value: bool) -> None:
| ^^^^^ FBT001
92 | pass
90 | pass
|
FBT.py:101:10: FBT001 Boolean-typed positional argument in function definition
FBT.py:99:10: FBT001 Boolean-typed positional argument in function definition
|
101 | def func(x: Union[list, Optional[int | str | float | bool]]):
99 | def func(x: Union[list, Optional[int | str | float | bool]]):
| ^ FBT001
102 | pass
100 | pass
|
FBT.py:105:10: FBT001 Boolean-typed positional argument in function definition
FBT.py:103:10: FBT001 Boolean-typed positional argument in function definition
|
105 | def func(x: bool | str):
103 | def func(x: bool | str):
| ^ FBT001
106 | pass
104 | pass
|

View File

@@ -1,10 +1,11 @@
use ruff_python_ast as ast;
use ruff_python_ast::{Arguments, Decorator};
use ruff_text_size::TextRange;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_semantic::{BindingKind, Scope, ScopeId};
use ruff_source_file::SourceRow;
use ruff_text_size::Ranged;
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
use crate::rules::flake8_builtins::helpers::shadows_builtin;
@@ -19,23 +20,6 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// non-obvious errors, as readers may mistake the attribute for the
/// builtin and vice versa.
///
/// Since methods and class attributes typically cannot be referenced directly
/// from outside the class scope, this rule only applies to those methods
/// and attributes that both shadow a builtin _and_ are referenced from within
/// the class scope, as in the following example, where the `list[int]` return
/// type annotation resolves to the `list` method, rather than the builtin:
///
/// ```python
/// class Class:
/// @staticmethod
/// def list() -> None:
/// pass
///
/// @staticmethod
/// def repeat(value: int, times: int) -> list[int]:
/// return [value] * times
/// ```
///
/// Builtins can be marked as exceptions to this rule via the
/// [`flake8-builtins.builtins-ignorelist`] configuration option, or
/// converted to the appropriate dunder method. Methods decorated with
@@ -44,112 +28,135 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
///
/// ## Example
/// ```python
/// class Class:
/// @staticmethod
/// def list() -> None:
/// pass
/// class Shadow:
/// def int():
/// return 0
/// ```
///
/// @staticmethod
/// def repeat(value: int, times: int) -> list[int]:
/// return [value] * times
/// Use instead:
/// ```python
/// class Shadow:
/// def to_int():
/// return 0
/// ```
///
/// Or:
/// ```python
/// class Shadow:
/// # Callable as `int(shadow)`
/// def __int__():
/// return 0
/// ```
///
/// ## Options
/// - `flake8-builtins.builtins-ignorelist`
///
/// ## References
/// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide)
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
#[violation]
pub struct BuiltinAttributeShadowing {
kind: Kind,
name: String,
row: SourceRow,
}
impl Violation for BuiltinAttributeShadowing {
#[derive_message_formats]
fn message(&self) -> String {
let BuiltinAttributeShadowing { kind, name, row } = self;
match kind {
Kind::Attribute => {
format!("Python builtin is shadowed by class attribute `{name}` from {row}")
}
Kind::Method => {
format!("Python builtin is shadowed by method `{name}` from {row}")
}
}
let BuiltinAttributeShadowing { name } = self;
format!("Class attribute `{name}` is shadowing a Python builtin")
}
}
/// A003
pub(crate) fn builtin_attribute_shadowing(
checker: &Checker,
scope_id: ScopeId,
scope: &Scope,
checker: &mut Checker,
class_def: &ast::StmtClassDef,
diagnostics: &mut Vec<Diagnostic>,
name: &str,
range: TextRange,
) {
for (name, binding_id) in scope.all_bindings() {
let binding = checker.semantic().binding(binding_id);
// We only care about methods and attributes.
let kind = match binding.kind {
BindingKind::Assignment | BindingKind::Annotation => Kind::Attribute,
BindingKind::FunctionDefinition(_) => Kind::Method,
_ => continue,
};
if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
) {
// Ignore explicit overrides.
if class_def.decorator_list.iter().any(|decorator| {
checker
.semantic()
.match_typing_expr(&decorator.expression, "override")
}) {
return;
}
// Class scopes are special, in that you can only reference a binding defined in a
// class scope from within the class scope itself. As such, we can safely ignore
// methods that weren't referenced from within the class scope. In other words, we're
// only trying to identify shadowing as in:
// ```python
// class Class:
// @staticmethod
// def list() -> None:
// pass
//
// @staticmethod
// def repeat(value: int, times: int) -> list[int]:
// return [value] * times
// ```
for reference in binding
.references
.iter()
.map(|reference_id| checker.semantic().reference(*reference_id))
.filter(|reference| {
checker
.semantic()
.first_non_type_parent_scope_id(reference.scope_id())
== Some(scope_id)
})
{
diagnostics.push(Diagnostic::new(
BuiltinAttributeShadowing {
kind,
name: name.to_string(),
row: checker.compute_source_row(binding.start()),
},
reference.range(),
));
}
if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
) {
// Ignore shadowing within `TypedDict` definitions, since these are only accessible through
// subscripting and not through attribute access.
if class_def
.bases()
.iter()
.any(|base| checker.semantic().match_typing_expr(base, "TypedDict"))
{
return;
}
checker.diagnostics.push(Diagnostic::new(
BuiltinAttributeShadowing {
name: name.to_string(),
},
range,
));
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Kind {
Attribute,
Method,
/// A003
pub(crate) fn builtin_method_shadowing(
checker: &mut Checker,
class_def: &ast::StmtClassDef,
name: &str,
decorator_list: &[Decorator],
range: TextRange,
) {
if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
) {
// Ignore some standard-library methods. Ideally, we'd ignore all overridden methods, since
// those should be flagged on the superclass, but that's more difficult.
if is_standard_library_override(name, class_def, checker.semantic()) {
return;
}
// Ignore explicit overrides.
if decorator_list.iter().any(|decorator| {
checker
.semantic()
.match_typing_expr(&decorator.expression, "override")
}) {
return;
}
checker.diagnostics.push(Diagnostic::new(
BuiltinAttributeShadowing {
name: name.to_string(),
},
range,
));
}
}
/// Return `true` if an attribute appears to be an override of a standard-library method.
fn is_standard_library_override(
name: &str,
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
) -> bool {
let Some(Arguments { args: bases, .. }) = class_def.arguments.as_deref() else {
return false;
};
match name {
// Ex) `Event.set`
"set" => bases.iter().any(|base| {
semantic
.resolve_call_path(base)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["threading", "Event"]))
}),
// Ex) `Filter.filter`
"filter" => bases.iter().any(|base| {
semantic
.resolve_call_path(base)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["logging", "Filter"]))
}),
_ => false,
}
}

View File

@@ -1,22 +1,68 @@
---
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
---
A003.py:17:31: A003 Python builtin is shadowed by method `str` from line 14
A003.py:2:5: A003 Class attribute `ImportError` is shadowing a Python builtin
|
1 | class MyClass:
2 | ImportError = 4
| ^^^^^^^^^^^ A003
3 | id: int
4 | dir = "/"
|
A003.py:3:5: A003 Class attribute `id` is shadowing a Python builtin
|
1 | class MyClass:
2 | ImportError = 4
3 | id: int
| ^^ A003
4 | dir = "/"
|
A003.py:4:5: A003 Class attribute `dir` is shadowing a Python builtin
|
2 | ImportError = 4
3 | id: int
4 | dir = "/"
| ^^^ A003
5 |
6 | def __init__(self):
|
A003.py:11:9: A003 Class attribute `str` is shadowing a Python builtin
|
15 | pass
16 |
17 | def method_usage(self) -> str:
| ^^^ A003
18 | pass
9 | self.dir = "."
10 |
11 | def str(self):
| ^^^ A003
12 | pass
|
A003.py:20:34: A003 Python builtin is shadowed by class attribute `id` from line 3
A003.py:29:9: A003 Class attribute `str` is shadowing a Python builtin
|
18 | pass
19 |
20 | def attribute_usage(self) -> id:
| ^^ A003
21 | pass
27 | ...
28 |
29 | def str(self) -> None:
| ^^^ A003
30 | ...
|
A003.py:40:9: A003 Class attribute `str` is shadowing a Python builtin
|
38 | ...
39 |
40 | def str(self) -> None:
| ^^^ A003
41 | ...
|
A003.py:52:9: A003 Class attribute `int` is shadowing a Python builtin
|
50 | pass
51 |
52 | def int(self):
| ^^^ A003
53 | pass
|

View File

@@ -1,13 +1,49 @@
---
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
---
A003.py:17:31: A003 Python builtin is shadowed by method `str` from line 14
A003.py:2:5: A003 Class attribute `ImportError` is shadowing a Python builtin
|
1 | class MyClass:
2 | ImportError = 4
| ^^^^^^^^^^^ A003
3 | id: int
4 | dir = "/"
|
A003.py:11:9: A003 Class attribute `str` is shadowing a Python builtin
|
15 | pass
16 |
17 | def method_usage(self) -> str:
| ^^^ A003
18 | pass
9 | self.dir = "."
10 |
11 | def str(self):
| ^^^ A003
12 | pass
|
A003.py:29:9: A003 Class attribute `str` is shadowing a Python builtin
|
27 | ...
28 |
29 | def str(self) -> None:
| ^^^ A003
30 | ...
|
A003.py:40:9: A003 Class attribute `str` is shadowing a Python builtin
|
38 | ...
39 |
40 | def str(self) -> None:
| ^^^ A003
41 | ...
|
A003.py:52:9: A003 Class attribute `int` is shadowing a Python builtin
|
50 | pass
51 |
52 | def int(self):
| ^^^ A003
53 | pass
|

View File

@@ -367,7 +367,7 @@ pub(crate) fn fix_unnecessary_literal_dict(expr: &Expr, checker: &Checker) -> Re
comma,
} = element
{
if let Some(Element::Simple { value: key, .. }) = tuple.elements.first() {
if let Some(Element::Simple { value: key, .. }) = tuple.elements.get(0) {
if let Some(Element::Simple { value, .. }) = tuple.elements.get(1) {
return Ok(DictElement::Simple {
key: key.clone(),

View File

@@ -134,7 +134,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
format!("Indices should only contain `{attr_name}` calls")
)
};
args.first()
args.get(0)
.unwrap_or_else(|| panic!("`{attr_name}` should have one argument"))
})
.collect();

View File

@@ -2,7 +2,6 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::{self as ast, StringLike};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -45,14 +44,8 @@ impl AlwaysFixableViolation for StringOrBytesTooLong {
/// PYI053
pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, string: StringLike) {
let semantic = checker.semantic();
// Ignore docstrings.
if is_docstring_stmt(semantic.current_statement()) {
return;
}
if is_warnings_dot_deprecated(semantic.current_expression_parent(), semantic) {
if is_docstring_stmt(checker.semantic().current_statement()) {
return;
}
@@ -74,21 +67,3 @@ pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, string: StringLike
)));
checker.diagnostics.push(diagnostic);
}
fn is_warnings_dot_deprecated(expr: Option<&ast::Expr>, semantic: &SemanticModel) -> bool {
// Does `expr` represent a call to `warnings.deprecated` or `typing_extensions.deprecated`?
let Some(expr) = expr else {
return false;
};
let Some(call) = expr.as_call_expr() else {
return false;
};
semantic
.resolve_call_path(&call.func)
.is_some_and(|call_path| {
matches!(
call_path.as_slice(),
["warnings" | "typing_extensions", "deprecated"]
)
})
}

View File

@@ -1,148 +1,128 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI053.pyi:7:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
PYI053.pyi:3:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
5 | def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
6 | def f2(
7 | x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
1 | def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
2 | def f2(
3 | x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
8 | ) -> None: ...
9 | def f3(
4 | ) -> None: ...
5 | def f3(
|
= help: Replace with `...`
Safe fix
4 4 |
5 5 | def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
6 6 | def f2(
7 |- x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
7 |+ x: str = ..., # Error: PYI053
8 8 | ) -> None: ...
9 9 | def f3(
10 10 | x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK
1 1 | def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
2 2 | def f2(
3 |- x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
3 |+ x: str = ..., # Error: PYI053
4 4 | ) -> None: ...
5 5 | def f3(
6 6 | x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK
PYI053.pyi:13:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
PYI053.pyi:9:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
11 | ) -> None: ...
12 | def f4(
13 | x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053
7 | ) -> None: ...
8 | def f4(
9 | x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
14 | ) -> None: ...
15 | def f5(
10 | ) -> None: ...
11 | def f5(
|
= help: Replace with `...`
Safe fix
10 10 | x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK
11 11 | ) -> None: ...
12 12 | def f4(
13 |- x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053
13 |+ x: str = ..., # Error: PYI053
14 14 | ) -> None: ...
15 15 | def f5(
16 16 | x: bytes = b"50 character byte stringgggggggggggggggggggggggggg", # OK
6 6 | x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK
7 7 | ) -> None: ...
8 8 | def f4(
9 |- x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053
9 |+ x: str = ..., # Error: PYI053
10 10 | ) -> None: ...
11 11 | def f5(
12 12 | x: bytes = b"50 character byte stringgggggggggggggggggggggggggg", # OK
PYI053.pyi:25:16: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
PYI053.pyi:21:16: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
23 | ) -> None: ...
24 | def f8(
25 | x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053
19 | ) -> None: ...
20 | def f8(
21 | x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
26 | ) -> None: ...
22 | ) -> None: ...
|
= help: Replace with `...`
Safe fix
22 22 | x: bytes = b"50 character byte stringggggggggggggggggggggggggg\xff", # OK
23 23 | ) -> None: ...
24 24 | def f8(
25 |- x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053
25 |+ x: bytes = ..., # Error: PYI053
26 26 | ) -> None: ...
27 27 |
28 28 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
18 18 | x: bytes = b"50 character byte stringggggggggggggggggggggggggg\xff", # OK
19 19 | ) -> None: ...
20 20 | def f8(
21 |- x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053
21 |+ x: bytes = ..., # Error: PYI053
22 22 | ) -> None: ...
23 23 |
24 24 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
PYI053.pyi:30:12: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
PYI053.pyi:26:12: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
28 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
29 |
30 | bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
24 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
25 |
26 | bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
27 |
28 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
|
= help: Replace with `...`
Safe fix
23 23 |
24 24 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
25 25 |
26 |-bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
26 |+bar: str = ... # Error: PYI053
27 27 |
28 28 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
29 29 |
PYI053.pyi:30:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
28 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
29 |
30 | qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
31 |
32 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
32 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
|
= help: Replace with `...`
Safe fix
27 27 |
28 28 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
28 28 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
29 29 |
30 |-bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
30 |+bar: str = ... # Error: PYI053
30 |-qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
30 |+qux: bytes = ... # Error: PYI053
31 31 |
32 32 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
32 32 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
33 33 |
PYI053.pyi:34:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
PYI053.pyi:34:15: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
32 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
32 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
33 |
34 | qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
34 | fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
35 |
36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
36 | class Demo:
|
= help: Replace with `...`
Safe fix
31 31 |
32 32 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
32 32 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
33 33 |
34 |-qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
34 |+qux: bytes = ... # Error: PYI053
34 |-fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
34 |+fbar: str = f"..." # Error: PYI053
35 35 |
36 36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
37 37 |
PYI053.pyi:38:15: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
37 |
38 | fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
39 |
40 | class Demo:
|
= help: Replace with `...`
Safe fix
35 35 |
36 36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
37 37 |
38 |-fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
38 |+fbar: str = f"..." # Error: PYI053
39 39 |
40 40 | class Demo:
41 41 | """Docstrings are excluded from this rule. Some padding.""" # OK
PYI053.pyi:64:5: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
63 | @not_warnings_dot_deprecated(
64 | "Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
65 | )
66 | def not_a_deprecated_function() -> None: ...
|
= help: Replace with `...`
Safe fix
61 61 | ) -> Callable[[Callable[[], None]], Callable[[], None]]: ...
62 62 |
63 63 | @not_warnings_dot_deprecated(
64 |- "Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053
64 |+ ... # Error: PYI053
65 65 | )
66 66 | def not_a_deprecated_function() -> None: ...
36 36 | class Demo:
37 37 | """Docstrings are excluded from this rule. Some padding.""" # OK

View File

@@ -168,7 +168,7 @@ pub(crate) fn avoidable_escaped_quote(
match tok {
Tok::String {
value: string_contents,
value,
kind,
triple_quoted,
} => {
@@ -176,6 +176,8 @@ pub(crate) fn avoidable_escaped_quote(
continue;
}
let string_contents = locator.slice(&value);
// Check if we're using the preferred quotation style.
if !leading_quote(locator.slice(tok_range)).is_some_and(|text| {
contains_quote(text, quotes_settings.inline_quotes.as_char())
@@ -312,7 +314,7 @@ pub(crate) fn unnecessary_escaped_quote(
match tok {
Tok::String {
value: string_contents,
value,
kind,
triple_quoted,
} => {
@@ -320,6 +322,8 @@ pub(crate) fn unnecessary_escaped_quote(
continue;
}
let string_contents = locator.slice(&value);
let leading = match leading_quote(locator.slice(tok_range)) {
Some("\"") => Quote::Double,
Some("'") => Quote::Single,

View File

@@ -379,8 +379,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) {
..
}) = &values[indices[0]]
{
args.first()
.expect("`isinstance` should have two arguments")
args.get(0).expect("`isinstance` should have two arguments")
} else {
unreachable!("Indices should only contain `isinstance` calls")
};

View File

@@ -142,7 +142,7 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex
else {
return;
};
let Some(arg) = args.first() else {
let Some(arg) = args.get(0) else {
return;
};
let Expr::StringLiteral(ast::ExprStringLiteral { value: env_var, .. }) = arg else {
@@ -249,7 +249,7 @@ pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) {
if attr != "get" {
return;
}
let Some(key) = args.first() else {
let Some(key) = args.get(0) else {
return;
};
if !(key.is_literal_expr() || key.is_name_expr()) {

View File

@@ -15,7 +15,6 @@ pub(crate) use reimplemented_builtin::*;
pub(crate) use return_in_try_except_finally::*;
pub(crate) use suppressible_exception::*;
pub(crate) use yoda_conditions::*;
pub(crate) use zip_dict_keys_and_values::*;
mod ast_bool_op;
mod ast_expr;
@@ -35,4 +34,3 @@ mod reimplemented_builtin;
mod return_in_try_except_finally;
mod suppressible_exception;
mod yoda_conditions;
mod zip_dict_keys_and_values;

View File

@@ -1,130 +0,0 @@
use ast::{ExprAttribute, ExprName, Identifier};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Arguments, Expr, ExprCall};
use ruff_text_size::Ranged;
use crate::{checkers::ast::Checker, fix::snippet::SourceCodeSnippet};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_python_semantic::analyze::typing::is_dict;
/// ## What it does
/// Checks for use of `zip()` to iterate over keys and values of a dictionary at once.
///
/// ## Why is this bad?
/// The `dict` type provides an `.items()` method which is faster and more readable.
///
/// ## Example
/// ```python
/// flag_stars = {"USA": 50, "Slovenia": 3, "Panama": 2, "Australia": 6}
///
/// for country, stars in zip(flag_stars.keys(), flag_stars.values()):
/// print(f"{country}'s flag has {stars} stars.")
/// ```
///
/// Use instead:
/// ```python
/// flag_stars = {"USA": 50, "Slovenia": 3, "Panama": 2, "Australia": 6}
///
/// for country, stars in flag_stars.items():
/// print(f"{country}'s flag has {stars} stars.")
/// ```
///
/// ## References
/// - [Python documentation: `dict.items`](https://docs.python.org/3/library/stdtypes.html#dict.items)
#[violation]
pub struct ZipDictKeysAndValues {
expected: SourceCodeSnippet,
actual: SourceCodeSnippet,
}
impl AlwaysFixableViolation for ZipDictKeysAndValues {
#[derive_message_formats]
fn message(&self) -> String {
let ZipDictKeysAndValues { expected, actual } = self;
if let (Some(expected), Some(actual)) = (expected.full_display(), actual.full_display()) {
format!("Use `{expected}` instead of `{actual}`")
} else {
format!("Use `dict.items()` instead of `zip(dict.keys(), dict.values())`")
}
}
fn fix_title(&self) -> String {
let ZipDictKeysAndValues { expected, actual } = self;
if let (Some(expected), Some(actual)) = (expected.full_display(), actual.full_display()) {
format!("Replace `{actual}` with `{expected}`")
} else {
"Replace `zip(dict.keys(), dict.values())` with `dict.items()`".to_string()
}
}
}
/// SIM911
pub(crate) fn zip_dict_keys_and_values(checker: &mut Checker, expr: &ExprCall) {
let ExprCall {
func,
arguments: Arguments { args, keywords, .. },
..
} = expr;
match &keywords[..] {
[] => {}
[ast::Keyword {
arg: Some(name), ..
}] if name.as_str() == "strict" => {}
_ => return,
};
if matches!(func.as_ref(), Expr::Name(ExprName { id, .. }) if id != "zip") {
return;
}
let [arg1, arg2] = &args[..] else {
return;
};
let Some((var1, attr1)) = get_var_attr(arg1) else {
return;
};
let Some((var2, attr2)) = get_var_attr(arg2) else {
return;
};
if var1.id != var2.id || attr1 != "keys" || attr2 != "values" {
return;
}
let Some(binding) = checker
.semantic()
.only_binding(var1)
.map(|id| checker.semantic().binding(id))
else {
return;
};
if !is_dict(binding, checker.semantic()) {
return;
}
let expected = format!("{}.items()", checker.locator().slice(var1));
let actual = checker.locator().slice(expr);
let mut diagnostic = Diagnostic::new(
ZipDictKeysAndValues {
expected: SourceCodeSnippet::new(expected.clone()),
actual: SourceCodeSnippet::from_str(actual),
},
expr.range(),
);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
expected,
expr.range(),
)));
checker.diagnostics.push(diagnostic);
}
fn get_var_attr(expr: &Expr) -> Option<(&ExprName, &Identifier)> {
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
return None;
};
let Expr::Attribute(ExprAttribute { value, attr, .. }) = func.as_ref() else {
return None;
};
let Expr::Name(var_name) = value.as_ref() else {
return None;
};
Some((var_name, attr))
}

View File

@@ -27,27 +27,4 @@ sections.py:144:5: D214 [*] Section is over-indented ("Returns")
148 148 | A value of some sort.
149 149 |
sections.py:558:5: D214 [*] Section is over-indented ("Returns")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D214
|
= help: Remove over-indentation from "Returns"
Safe fix
560 560 | Args:
561 561 | Here's a note.
562 562 |
563 |- Returns:
563 |+ Returns:
564 564 | """

View File

@@ -498,74 +498,4 @@ sections.py:527:5: D407 [*] Missing dashed underline after section ("Parameters"
531 532 | """
532 533 |
sections.py:548:5: D407 [*] Missing dashed underline after section ("Args")
|
547 | def lowercase_sub_section_header():
548 | """Below, `returns:` should _not_ be considered a section header.
| _____^
549 | |
550 | | Args:
551 | | Here's a note.
552 | |
553 | | returns:
554 | | """
| |_______^ D407
|
= help: Add dashed line under "Args"
Safe fix
548 548 | """Below, `returns:` should _not_ be considered a section header.
549 549 |
550 550 | Args:
551 |+ ----
551 552 | Here's a note.
552 553 |
553 554 | returns:
sections.py:558:5: D407 [*] Missing dashed underline after section ("Args")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D407
|
= help: Add dashed line under "Args"
Safe fix
558 558 | """Below, `Returns:` should be considered a section header.
559 559 |
560 560 | Args:
561 |+ ----
561 562 | Here's a note.
562 563 |
563 564 | Returns:
sections.py:558:5: D407 [*] Missing dashed underline after section ("Returns")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D407
|
= help: Add dashed line under "Returns"
Safe fix
561 561 | Here's a note.
562 562 |
563 563 | Returns:
564 |+ -------
564 565 | """

View File

@@ -97,18 +97,4 @@ sections.py:261:5: D414 Section has no content ("Returns")
| |_______^ D414
|
sections.py:558:5: D414 Section has no content ("Returns")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D414
|

View File

@@ -64,12 +64,4 @@ D417.py:108:5: D417 Missing argument description in the docstring for `f`: `*arg
109 | """Do something.
|
D417.py:155:5: D417 Missing argument description in the docstring for `select_data`: `auto_save`
|
155 | def select_data(
| ^^^^^^^^^^^ D417
156 | query: str,
157 | args: tuple,
|

View File

@@ -64,12 +64,4 @@ D417.py:108:5: D417 Missing argument description in the docstring for `f`: `*arg
109 | """Do something.
|
D417.py:155:5: D417 Missing argument description in the docstring for `select_data`: `auto_save`
|
155 | def select_data(
| ^^^^^^^^^^^ D417
156 | query: str,
157 | args: tuple,
|

View File

@@ -120,7 +120,6 @@ mod tests {
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_24.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_25.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_26.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_27.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_0.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_1.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_2.py"))]
@@ -168,34 +167,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_0.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_1.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_10.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_11.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_12.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_13.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_14.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_15.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_16.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_17.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_18.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_19.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_2.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_20.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_21.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_22.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_23.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_24.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_25.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_26.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_27.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_3.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_4.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_5.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_6.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_7.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_8.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_9.py"))]
#[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(

View File

@@ -4,8 +4,8 @@ use ruff_python_ast::{CmpOp, Expr};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers;
use ruff_python_parser::{lexer, Mode, Tok};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_python_parser::locate_cmp_ops;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::settings::types::PreviewMode;
@@ -138,207 +138,3 @@ impl From<&CmpOp> for IsCmpOp {
}
}
}
/// Extract all [`CmpOp`] operators from an expression snippet, with appropriate
/// ranges.
///
/// `RustPython` doesn't include line and column information on [`CmpOp`] nodes.
/// `CPython` doesn't either. This method iterates over the token stream and
/// re-identifies [`CmpOp`] nodes, annotating them with valid ranges.
fn locate_cmp_ops(expr: &Expr, source: &str) -> Vec<LocatedCmpOp> {
// If `Expr` is a multi-line expression, we need to parenthesize it to
// ensure that it's lexed correctly.
let contents = &source[expr.range()];
let parenthesized_contents = format!("({contents})");
let mut tok_iter = lexer::lex(&parenthesized_contents, Mode::Expression)
.flatten()
.skip(1)
.map(|(tok, range)| (tok, range - TextSize::from(1)))
.filter(|(tok, _)| !matches!(tok, Tok::NonLogicalNewline | Tok::Comment(_)))
.peekable();
let mut ops: Vec<LocatedCmpOp> = vec![];
// Track the bracket depth.
let mut par_count = 0u32;
let mut sqb_count = 0u32;
let mut brace_count = 0u32;
loop {
let Some((tok, range)) = tok_iter.next() else {
break;
};
match tok {
Tok::Lpar => {
par_count = par_count.saturating_add(1);
}
Tok::Rpar => {
par_count = par_count.saturating_sub(1);
}
Tok::Lsqb => {
sqb_count = sqb_count.saturating_add(1);
}
Tok::Rsqb => {
sqb_count = sqb_count.saturating_sub(1);
}
Tok::Lbrace => {
brace_count = brace_count.saturating_add(1);
}
Tok::Rbrace => {
brace_count = brace_count.saturating_sub(1);
}
_ => {}
}
if par_count > 0 || sqb_count > 0 || brace_count > 0 {
continue;
}
match tok {
Tok::Not => {
if let Some((_, next_range)) = tok_iter.next_if(|(tok, _)| tok.is_in()) {
ops.push(LocatedCmpOp::new(
TextRange::new(range.start(), next_range.end()),
CmpOp::NotIn,
));
}
}
Tok::In => {
ops.push(LocatedCmpOp::new(range, CmpOp::In));
}
Tok::Is => {
let op = if let Some((_, next_range)) = tok_iter.next_if(|(tok, _)| tok.is_not()) {
LocatedCmpOp::new(
TextRange::new(range.start(), next_range.end()),
CmpOp::IsNot,
)
} else {
LocatedCmpOp::new(range, CmpOp::Is)
};
ops.push(op);
}
Tok::NotEqual => {
ops.push(LocatedCmpOp::new(range, CmpOp::NotEq));
}
Tok::EqEqual => {
ops.push(LocatedCmpOp::new(range, CmpOp::Eq));
}
Tok::GreaterEqual => {
ops.push(LocatedCmpOp::new(range, CmpOp::GtE));
}
Tok::Greater => {
ops.push(LocatedCmpOp::new(range, CmpOp::Gt));
}
Tok::LessEqual => {
ops.push(LocatedCmpOp::new(range, CmpOp::LtE));
}
Tok::Less => {
ops.push(LocatedCmpOp::new(range, CmpOp::Lt));
}
_ => {}
}
}
ops
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct LocatedCmpOp {
range: TextRange,
op: CmpOp,
}
impl LocatedCmpOp {
fn new<T: Into<TextRange>>(range: T, op: CmpOp) -> Self {
Self {
range: range.into(),
op,
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use ruff_python_ast::CmpOp;
use ruff_python_parser::parse_expression;
use ruff_text_size::TextSize;
use super::{locate_cmp_ops, LocatedCmpOp};
#[test]
fn extract_cmp_op_location() -> Result<()> {
let contents = "x == 1";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(4),
CmpOp::Eq
)]
);
let contents = "x != 1";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(4),
CmpOp::NotEq
)]
);
let contents = "x is 1";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(4),
CmpOp::Is
)]
);
let contents = "x is not 1";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(8),
CmpOp::IsNot
)]
);
let contents = "x in 1";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(4),
CmpOp::In
)]
);
let contents = "x not in 1";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(8),
CmpOp::NotIn
)]
);
let contents = "x != (1 is not 2)";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(4),
CmpOp::NotEq
)]
);
Ok(())
}
}

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{FixAvailability, Violation};
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_source_file::SourceRow;
@@ -29,16 +29,9 @@ pub struct RedefinedWhileUnused {
}
impl Violation for RedefinedWhileUnused {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let RedefinedWhileUnused { name, row } = self;
format!("Redefinition of unused `{name}` from {row}")
}
fn fix_title(&self) -> Option<String> {
let RedefinedWhileUnused { name, .. } = self;
Some(format!("Remove definition: `{name}`"))
}
}

View File

@@ -7,6 +7,5 @@ F811_0.py:10:5: F811 Redefinition of unused `bar` from line 6
| ^^^ F811
11 | pass
|
= help: Remove definition: `bar`

View File

@@ -6,6 +6,5 @@ F811_1.py:1:25: F811 Redefinition of unused `FU` from line 1
1 | import fu as FU, bar as FU
| ^^ F811
|
= help: Remove definition: `FU`

View File

@@ -9,6 +9,5 @@ F811_12.py:6:20: F811 Redefinition of unused `mixer` from line 2
| ^^^^^ F811
7 | mixer(123)
|
= help: Remove definition: `mixer`

View File

@@ -7,6 +7,5 @@ F811_15.py:4:5: F811 Redefinition of unused `fu` from line 1
| ^^ F811
5 | pass
|
= help: Remove definition: `fu`

View File

@@ -9,6 +9,5 @@ F811_16.py:8:13: F811 Redefinition of unused `fu` from line 3
| ^^ F811
9 | pass
|
= help: Remove definition: `fu`

View File

@@ -9,7 +9,6 @@ F811_17.py:6:12: F811 Redefinition of unused `fu` from line 2
7 |
8 | def baz():
|
= help: Remove definition: `fu`
F811_17.py:9:13: F811 Redefinition of unused `fu` from line 6
|
@@ -18,6 +17,5 @@ F811_17.py:9:13: F811 Redefinition of unused `fu` from line 6
| ^^ F811
10 | pass
|
= help: Remove definition: `fu`

View File

@@ -6,6 +6,5 @@ F811_2.py:1:34: F811 Redefinition of unused `FU` from line 1
1 | from moo import fu as FU, bar as FU
| ^^ F811
|
= help: Remove definition: `FU`

View File

@@ -9,6 +9,5 @@ F811_21.py:32:5: F811 Redefinition of unused `Sequence` from line 26
| ^^^^^^^^ F811
33 | )
|
= help: Remove definition: `Sequence`

View File

@@ -7,6 +7,5 @@ F811_23.py:4:15: F811 Redefinition of unused `foo` from line 3
4 | import bar as foo
| ^^^ F811
|
= help: Remove definition: `foo`

View File

@@ -9,6 +9,5 @@ F811_26.py:5:9: F811 Redefinition of unused `func` from line 2
| ^^^^ F811
6 | pass
|
= help: Remove definition: `func`

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -6,6 +6,5 @@ F811_3.py:1:12: F811 Redefinition of unused `fu` from line 1
1 | import fu; fu = 3
| ^^ F811
|
= help: Remove definition: `fu`

View File

@@ -6,6 +6,5 @@ F811_4.py:1:12: F811 Redefinition of unused `fu` from line 1
1 | import fu; fu, bar = 3
| ^^ F811
|
= help: Remove definition: `fu`

View File

@@ -6,6 +6,5 @@ F811_5.py:1:13: F811 Redefinition of unused `fu` from line 1
1 | import fu; [fu, bar] = 3
| ^^ F811
|
= help: Remove definition: `fu`

View File

@@ -9,6 +9,5 @@ F811_6.py:6:12: F811 Redefinition of unused `os` from line 5
| ^^ F811
7 | os.path
|
= help: Remove definition: `os`

View File

@@ -10,6 +10,5 @@ F811_8.py:5:12: F811 Redefinition of unused `os` from line 4
6 | except:
7 | pass
|
= help: Remove definition: `os`

View File

@@ -25,6 +25,5 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs
6 |
7 | # Despite this `del`, `import os` in `f` should still be flagged as shadowing an unused
|
= help: Remove definition: `os`

View File

@@ -24,6 +24,5 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs
| ^^ F811
6 | print(os)
|
= help: Remove definition: `os`

View File

@@ -10,6 +10,5 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs
5 |
6 | # Despite this `del`, `import os` should still be flagged as shadowing an unused
|
= help: Remove definition: `os`

View File

@@ -1,12 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_0.py:10:5: F811 Redefinition of unused `bar` from line 6
|
10 | def bar():
| ^^^ F811
11 | pass
|
= help: Remove definition: `bar`

View File

@@ -1,15 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_1.py:1:25: F811 [*] Redefinition of unused `FU` from line 1
|
1 | import fu as FU, bar as FU
| ^^ F811
|
= help: Remove definition: `FU`
Safe fix
1 |-import fu as FU, bar as FU
1 |+import fu as FU

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -1,22 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_12.py:6:20: F811 [*] Redefinition of unused `mixer` from line 2
|
4 | pass
5 | else:
6 | from bb import mixer
| ^^^^^ F811
7 | mixer(123)
|
= help: Remove definition: `mixer`
Safe fix
3 3 | except ImportError:
4 4 | pass
5 5 | else:
6 |- from bb import mixer
6 |+ pass
7 7 | mixer(123)

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -1,12 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_15.py:4:5: F811 Redefinition of unused `fu` from line 1
|
4 | def fu():
| ^^ F811
5 | pass
|
= help: Remove definition: `fu`

View File

@@ -1,14 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_16.py:8:13: F811 Redefinition of unused `fu` from line 3
|
6 | def bar():
7 | def baz():
8 | def fu():
| ^^ F811
9 | pass
|
= help: Remove definition: `fu`

View File

@@ -1,32 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_17.py:6:12: F811 [*] Redefinition of unused `fu` from line 2
|
5 | def bar():
6 | import fu
| ^^ F811
7 |
8 | def baz():
|
= help: Remove definition: `fu`
Safe fix
3 3 |
4 4 |
5 5 | def bar():
6 |- import fu
7 6 |
8 7 | def baz():
9 8 | def fu():
F811_17.py:9:13: F811 Redefinition of unused `fu` from line 6
|
8 | def baz():
9 | def fu():
| ^^ F811
10 | pass
|
= help: Remove definition: `fu`

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -1,15 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_2.py:1:34: F811 [*] Redefinition of unused `FU` from line 1
|
1 | from moo import fu as FU, bar as FU
| ^^ F811
|
= help: Remove definition: `FU`
Safe fix
1 |-from moo import fu as FU, bar as FU
1 |+from moo import fu as FU

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -1,25 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_21.py:32:5: F811 [*] Redefinition of unused `Sequence` from line 26
|
30 | from typing import (
31 | List, # noqa: F811
32 | Sequence,
| ^^^^^^^^ F811
33 | )
|
= help: Remove definition: `Sequence`
Safe fix
29 29 | # This should ignore the first error.
30 30 | from typing import (
31 31 | List, # noqa: F811
32 |- Sequence,
33 |-)
32 |+ )
34 33 |
35 34 | # This should ignore both errors.
36 35 | from typing import ( # noqa

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -1,18 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_23.py:4:15: F811 [*] Redefinition of unused `foo` from line 3
|
3 | import foo as foo
4 | import bar as foo
| ^^^ F811
|
= help: Remove definition: `foo`
Safe fix
1 1 | """Test that shadowing an explicit re-export produces a warning."""
2 2 |
3 3 | import foo as foo
4 |-import bar as foo

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -1,14 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_26.py:5:9: F811 Redefinition of unused `func` from line 2
|
3 | pass
4 |
5 | def func(self):
| ^^^^ F811
6 | pass
|
= help: Remove definition: `func`

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -1,11 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_3.py:1:12: F811 Redefinition of unused `fu` from line 1
|
1 | import fu; fu = 3
| ^^ F811
|
= help: Remove definition: `fu`

View File

@@ -1,11 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_4.py:1:12: F811 Redefinition of unused `fu` from line 1
|
1 | import fu; fu, bar = 3
| ^^ F811
|
= help: Remove definition: `fu`

View File

@@ -1,11 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_5.py:1:13: F811 Redefinition of unused `fu` from line 1
|
1 | import fu; [fu, bar] = 3
| ^^ F811
|
= help: Remove definition: `fu`

View File

@@ -1,21 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_6.py:6:12: F811 [*] Redefinition of unused `os` from line 5
|
4 | if i == 1:
5 | import os
6 | import os
| ^^ F811
7 | os.path
|
= help: Remove definition: `os`
Safe fix
3 3 | i = 2
4 4 | if i == 1:
5 5 | import os
6 |- import os
7 6 | os.path

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

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