Compare commits
5 Commits
simplify-S
...
charlie/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b54b1c1f05 | ||
|
|
ad1b5c52d5 | ||
|
|
bd3fe93f41 | ||
|
|
3c7cbbcb92 | ||
|
|
41f1f8a4ca |
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -1321,6 +1321,12 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.4"
|
||||
@@ -1535,6 +1541,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "path-slash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.1"
|
||||
@@ -2490,8 +2502,10 @@ dependencies = [
|
||||
"is-macro",
|
||||
"itertools 0.12.0",
|
||||
"log",
|
||||
"matchit",
|
||||
"once_cell",
|
||||
"path-absolutize",
|
||||
"path-slash",
|
||||
"pep440_rs 0.4.0",
|
||||
"regex",
|
||||
"ruff_cache",
|
||||
|
||||
@@ -55,12 +55,14 @@ lalrpop-util = { version = "0.20.0", default-features = false }
|
||||
lexical-parse-float = { version = "0.8.0", features = ["format"] }
|
||||
libcst = { version = "1.1.0", default-features = false }
|
||||
log = { version = "0.4.17" }
|
||||
matchit = { version = "0.7.3" }
|
||||
memchr = { version = "2.6.4" }
|
||||
mimalloc = { version ="0.1.39"}
|
||||
natord = { version = "1.0.9" }
|
||||
notify = { version = "6.1.1" }
|
||||
once_cell = { version = "1.19.0" }
|
||||
path-absolutize = { version = "3.1.1" }
|
||||
path-slash = { version = "0.2.1" }
|
||||
pathdiff = { version = "0.2.1" }
|
||||
pep440_rs = { version = "0.4.0", features = ["serde"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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`].
|
||||
@@ -442,7 +443,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 +469,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 +499,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)
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
)?;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
@@ -459,17 +452,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 +469,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 +526,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 +544,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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
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`
|
||||
@@ -31,8 +30,7 @@ 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]
|
||||
z = [a for a in range(5) if a or b or c or d and e] # RUF021: => `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`
|
||||
@@ -41,20 +39,6 @@ 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
|
||||
#############################################
|
||||
|
||||
@@ -7,8 +7,7 @@ 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 +27,6 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
Rule::UndefinedLocal,
|
||||
Rule::UnusedAnnotation,
|
||||
Rule::UnusedClassMethodArgument,
|
||||
Rule::BuiltinAttributeShadowing,
|
||||
Rule::UnusedFunctionArgument,
|
||||
Rule::UnusedImport,
|
||||
Rule::UnusedLambdaArgument,
|
||||
@@ -299,18 +297,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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -332,12 +332,7 @@ const BACKPORTS_STR_ENUM_TO_ENUM_311: &[&str] = &["StrEnum"];
|
||||
|
||||
// Members of `typing_extensions` that were moved to `typing`.
|
||||
const TYPING_EXTENSIONS_TO_TYPING_312: &[&str] = &[
|
||||
// Introduced in Python 3.12, but `typing_extensions` backports some bug fixes.
|
||||
// "NamedTuple",
|
||||
|
||||
// Introduced in Python 3.12, but `typing_extensions` backports support for PEP 705.
|
||||
// "TypedDict",
|
||||
|
||||
"NamedTuple",
|
||||
// Introduced in Python 3.8, but `typing_extensions` backports a ton of optimizations that were
|
||||
// added in Python 3.12.
|
||||
"Protocol",
|
||||
@@ -347,20 +342,13 @@ const TYPING_EXTENSIONS_TO_TYPING_312: &[&str] = &[
|
||||
"SupportsFloat",
|
||||
"SupportsInt",
|
||||
"SupportsRound",
|
||||
"TypeAliasType",
|
||||
"TypedDict",
|
||||
"Unpack",
|
||||
// Introduced in Python 3.11, but `typing_extensions` backports the `frozen_default` argument,
|
||||
// which was introduced in Python 3.12.
|
||||
"dataclass_transform",
|
||||
"override",
|
||||
];
|
||||
|
||||
// Members of `typing_extensions` that were moved to `collections.abc`.
|
||||
const TYPING_EXTENSIONS_TO_COLLECTIONS_ABC_312: &[&str] = &["Buffer"];
|
||||
|
||||
// Members of `typing_extensions` that were moved to `types`.
|
||||
const TYPING_EXTENSIONS_TO_TYPES_312: &[&str] = &["get_original_bases"];
|
||||
|
||||
struct ImportReplacer<'a> {
|
||||
stmt: &'a Stmt,
|
||||
module: &'a str,
|
||||
@@ -429,28 +417,6 @@ impl<'a> ImportReplacer<'a> {
|
||||
}
|
||||
}
|
||||
"typing_extensions" => {
|
||||
// `typing_extensions` to `collections.abc`
|
||||
let mut typing_extensions_to_collections_abc = vec![];
|
||||
if self.version >= PythonVersion::Py312 {
|
||||
typing_extensions_to_collections_abc
|
||||
.extend(TYPING_EXTENSIONS_TO_COLLECTIONS_ABC_312);
|
||||
}
|
||||
if let Some(operation) =
|
||||
self.try_replace(&typing_extensions_to_collections_abc, "collections.abc")
|
||||
{
|
||||
operations.push(operation);
|
||||
}
|
||||
|
||||
// `typing_extensions` to `types`
|
||||
let mut typing_extensions_to_types = vec![];
|
||||
if self.version >= PythonVersion::Py312 {
|
||||
typing_extensions_to_types.extend(TYPING_EXTENSIONS_TO_TYPES_312);
|
||||
}
|
||||
if let Some(operation) = self.try_replace(&typing_extensions_to_types, "types") {
|
||||
operations.push(operation);
|
||||
}
|
||||
|
||||
// `typing_extensions` to `typing`
|
||||
let mut typing_extensions_to_typing = TYPING_EXTENSIONS_TO_TYPING.to_vec();
|
||||
if self.version >= PythonVersion::Py37 {
|
||||
typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_37);
|
||||
|
||||
@@ -995,6 +995,26 @@ UP035.py:77:1: UP035 [*] Import from `collections.abc` instead: `Callable`
|
||||
79 79 |
|
||||
80 80 | # OK
|
||||
|
||||
UP035.py:87:1: UP035 [*] Import from `typing` instead: `NamedTuple`
|
||||
|
|
||||
86 | # OK: `typing_extensions` contains backported improvements.
|
||||
87 | from typing_extensions import NamedTuple
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||
88 |
|
||||
89 | # OK: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
||||
|
|
||||
= help: Import from `typing`
|
||||
|
||||
ℹ Safe fix
|
||||
84 84 | from typing_extensions import SupportsIndex
|
||||
85 85 |
|
||||
86 86 | # OK: `typing_extensions` contains backported improvements.
|
||||
87 |-from typing_extensions import NamedTuple
|
||||
87 |+from typing import NamedTuple
|
||||
88 88 |
|
||||
89 89 | # OK: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
||||
90 90 | from typing_extensions import dataclass_transform
|
||||
|
||||
UP035.py:90:1: UP035 [*] Import from `typing` instead: `dataclass_transform`
|
||||
|
|
||||
89 | # OK: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
||||
@@ -1020,8 +1040,6 @@ UP035.py:93:1: UP035 [*] Import from `enum` instead: `StrEnum`
|
||||
92 | # UP035
|
||||
93 | from backports.strenum import StrEnum
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||
94 |
|
||||
95 | # UP035
|
||||
|
|
||||
= help: Import from `enum`
|
||||
|
||||
@@ -1031,63 +1049,5 @@ UP035.py:93:1: UP035 [*] Import from `enum` instead: `StrEnum`
|
||||
92 92 | # UP035
|
||||
93 |-from backports.strenum import StrEnum
|
||||
93 |+from enum import StrEnum
|
||||
94 94 |
|
||||
95 95 | # UP035
|
||||
96 96 | from typing_extensions import override
|
||||
|
||||
UP035.py:96:1: UP035 [*] Import from `typing` instead: `override`
|
||||
|
|
||||
95 | # UP035
|
||||
96 | from typing_extensions import override
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||
97 |
|
||||
98 | # UP035
|
||||
|
|
||||
= help: Import from `typing`
|
||||
|
||||
ℹ Safe fix
|
||||
93 93 | from backports.strenum import StrEnum
|
||||
94 94 |
|
||||
95 95 | # UP035
|
||||
96 |-from typing_extensions import override
|
||||
96 |+from typing import override
|
||||
97 97 |
|
||||
98 98 | # UP035
|
||||
99 99 | from typing_extensions import Buffer
|
||||
|
||||
UP035.py:99:1: UP035 [*] Import from `collections.abc` instead: `Buffer`
|
||||
|
|
||||
98 | # UP035
|
||||
99 | from typing_extensions import Buffer
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||
100 |
|
||||
101 | # UP035
|
||||
|
|
||||
= help: Import from `collections.abc`
|
||||
|
||||
ℹ Safe fix
|
||||
96 96 | from typing_extensions import override
|
||||
97 97 |
|
||||
98 98 | # UP035
|
||||
99 |-from typing_extensions import Buffer
|
||||
99 |+from collections.abc import Buffer
|
||||
100 100 |
|
||||
101 101 | # UP035
|
||||
102 102 | from typing_extensions import get_original_bases
|
||||
|
||||
UP035.py:102:1: UP035 [*] Import from `types` instead: `get_original_bases`
|
||||
|
|
||||
101 | # UP035
|
||||
102 | from typing_extensions import get_original_bases
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||
|
|
||||
= help: Import from `types`
|
||||
|
||||
ℹ Safe fix
|
||||
99 99 | from typing_extensions import Buffer
|
||||
100 100 |
|
||||
101 101 | # UP035
|
||||
102 |-from typing_extensions import get_original_bases
|
||||
102 |+from types import get_original_bases
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
@@ -36,17 +36,13 @@ use crate::checkers::ast::Checker;
|
||||
#[violation]
|
||||
pub struct ParenthesizeChainedOperators;
|
||||
|
||||
impl AlwaysFixableViolation for ParenthesizeChainedOperators {
|
||||
impl Violation for ParenthesizeChainedOperators {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear"
|
||||
)
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Parenthesize the `and` subexpression".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF021
|
||||
@@ -79,22 +75,18 @@ pub(crate) fn parenthesize_chained_logical_operators(
|
||||
..
|
||||
},
|
||||
) => {
|
||||
let locator = checker.locator();
|
||||
let source_range = bool_op.range();
|
||||
if parenthesized_range(
|
||||
bool_op.into(),
|
||||
expr.into(),
|
||||
checker.indexer().comment_ranges(),
|
||||
locator.contents(),
|
||||
checker.locator().contents(),
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
let new_source = format!("({})", locator.slice(source_range));
|
||||
let edit = Edit::range_replacement(new_source, source_range);
|
||||
checker.diagnostics.push(
|
||||
Diagnostic::new(ParenthesizeChainedOperators, source_range)
|
||||
.with_fix(Fix::safe_edit(edit)),
|
||||
);
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
ParenthesizeChainedOperators,
|
||||
bool_op.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
|
||||
@@ -1,259 +1,83 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF021.py:12:10: RUF021 [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
RUF021.py:12:10: RUF021 Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
|
|
||||
11 | a, b, c = 1, 0, 2
|
||||
12 | x = a or b and c # RUF021: => `a or (b and c)`
|
||||
| ^^^^^^^ RUF021
|
||||
13 | x = a or b and c # looooooooooooooooooooooooooooooong comment but it won't prevent an autofix
|
||||
13 |
|
||||
14 | a, b, c = 0, 1, 2
|
||||
|
|
||||
= help: Parenthesize the `and` subexpression
|
||||
|
||||
ℹ Safe fix
|
||||
9 9 | # as part of a chain.
|
||||
10 10 |
|
||||
11 11 | a, b, c = 1, 0, 2
|
||||
12 |-x = a or b and c # RUF021: => `a or (b and c)`
|
||||
12 |+x = a or (b and c) # RUF021: => `a or (b and c)`
|
||||
13 13 | x = a or b and c # looooooooooooooooooooooooooooooong comment but it won't prevent an autofix
|
||||
14 14 |
|
||||
15 15 | a, b, c = 0, 1, 2
|
||||
|
||||
RUF021.py:13:10: RUF021 [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
RUF021.py:15:5: RUF021 Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
|
|
||||
11 | a, b, c = 1, 0, 2
|
||||
12 | x = a or b and c # RUF021: => `a or (b and c)`
|
||||
13 | x = a or b and c # looooooooooooooooooooooooooooooong comment but it won't prevent an autofix
|
||||
| ^^^^^^^ RUF021
|
||||
14 |
|
||||
15 | a, b, c = 0, 1, 2
|
||||
|
|
||||
= help: Parenthesize the `and` subexpression
|
||||
|
||||
ℹ Safe fix
|
||||
10 10 |
|
||||
11 11 | a, b, c = 1, 0, 2
|
||||
12 12 | x = a or b and c # RUF021: => `a or (b and c)`
|
||||
13 |-x = a or b and c # looooooooooooooooooooooooooooooong comment but it won't prevent an autofix
|
||||
13 |+x = a or (b and c) # looooooooooooooooooooooooooooooong comment but it won't prevent an autofix
|
||||
14 14 |
|
||||
15 15 | a, b, c = 0, 1, 2
|
||||
16 16 | y = a and b or c # RUF021: => `(a and b) or c`
|
||||
|
||||
RUF021.py:16:5: RUF021 [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
|
|
||||
15 | a, b, c = 0, 1, 2
|
||||
16 | y = a and b or c # RUF021: => `(a and b) or c`
|
||||
14 | a, b, c = 0, 1, 2
|
||||
15 | y = a and b or c # RUF021: => `(a and b) or c`
|
||||
| ^^^^^^^ RUF021
|
||||
17 |
|
||||
18 | a, b, c, d = 1, 2, 0, 3
|
||||
16 |
|
||||
17 | a, b, c, d = 1, 2, 0, 3
|
||||
|
|
||||
= help: Parenthesize the `and` subexpression
|
||||
|
||||
ℹ Safe fix
|
||||
13 13 | x = a or b and c # looooooooooooooooooooooooooooooong comment but it won't prevent an autofix
|
||||
14 14 |
|
||||
15 15 | a, b, c = 0, 1, 2
|
||||
16 |-y = a and b or c # RUF021: => `(a and b) or c`
|
||||
16 |+y = (a and b) or c # RUF021: => `(a and b) or c`
|
||||
17 17 |
|
||||
18 18 | a, b, c, d = 1, 2, 0, 3
|
||||
19 19 | if a or b or c and d: # RUF021: => `a or b or (c and d)`
|
||||
|
||||
RUF021.py:19:14: RUF021 [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
RUF021.py:18:14: RUF021 Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
|
|
||||
18 | a, b, c, d = 1, 2, 0, 3
|
||||
19 | if a or b or c and d: # RUF021: => `a or b or (c and d)`
|
||||
17 | a, b, c, d = 1, 2, 0, 3
|
||||
18 | if a or b or c and d: # RUF021: => `a or b or (c and d)`
|
||||
| ^^^^^^^ RUF021
|
||||
20 | pass
|
||||
19 | pass
|
||||
|
|
||||
= help: Parenthesize the `and` subexpression
|
||||
|
||||
ℹ Safe fix
|
||||
16 16 | y = a and b or c # RUF021: => `(a and b) or c`
|
||||
17 17 |
|
||||
18 18 | a, b, c, d = 1, 2, 0, 3
|
||||
19 |-if a or b or c and d: # RUF021: => `a or b or (c and d)`
|
||||
19 |+if a or b or (c and d): # RUF021: => `a or b or (c and d)`
|
||||
20 20 | pass
|
||||
21 21 |
|
||||
22 22 | a, b, c, d = 0, 0, 2, 3
|
||||
|
||||
RUF021.py:26:11: RUF021 [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
RUF021.py:25:11: RUF021 Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
|
|
||||
24 | if bool():
|
||||
25 | pass
|
||||
26 | elif a or b and c or d: # RUF021: => `a or (b and c) or d`
|
||||
23 | if bool():
|
||||
24 | pass
|
||||
25 | elif a or b and c or d: # RUF021: => `a or (b and c) or d`
|
||||
| ^^^^^^^ RUF021
|
||||
27 | pass
|
||||
26 | pass
|
||||
|
|
||||
= help: Parenthesize the `and` subexpression
|
||||
|
||||
ℹ Safe fix
|
||||
23 23 |
|
||||
24 24 | if bool():
|
||||
25 25 | pass
|
||||
26 |-elif a or b and c or d: # RUF021: => `a or (b and c) or d`
|
||||
26 |+elif a or (b and c) or d: # RUF021: => `a or (b and c) or d`
|
||||
27 27 | pass
|
||||
28 28 |
|
||||
29 29 | a, b, c, d = 0, 1, 0, 2
|
||||
|
||||
RUF021.py:30:7: RUF021 [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
RUF021.py:29:7: RUF021 Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
|
|
||||
29 | a, b, c, d = 0, 1, 0, 2
|
||||
30 | while a and b or c and d: # RUF021: => `(and b) or (c and d)`
|
||||
28 | a, b, c, d = 0, 1, 0, 2
|
||||
29 | while a and b or c and d: # RUF021: => `(and b) or (c and d)`
|
||||
| ^^^^^^^ RUF021
|
||||
31 | pass
|
||||
30 | pass
|
||||
|
|
||||
= help: Parenthesize the `and` subexpression
|
||||
|
||||
ℹ Safe fix
|
||||
27 27 | pass
|
||||
28 28 |
|
||||
29 29 | a, b, c, d = 0, 1, 0, 2
|
||||
30 |-while a and b or c and d: # RUF021: => `(and b) or (c and d)`
|
||||
30 |+while (a and b) or c and d: # RUF021: => `(and b) or (c and d)`
|
||||
31 31 | pass
|
||||
32 32 |
|
||||
33 33 | b, c, d, e = 2, 3, 0, 4
|
||||
|
||||
RUF021.py:30:18: RUF021 [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
RUF021.py:29:18: RUF021 Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
|
|
||||
29 | a, b, c, d = 0, 1, 0, 2
|
||||
30 | while a and b or c and d: # RUF021: => `(and b) or (c and d)`
|
||||
28 | a, b, c, d = 0, 1, 0, 2
|
||||
29 | while a and b or c and d: # RUF021: => `(and b) or (c and d)`
|
||||
| ^^^^^^^ RUF021
|
||||
31 | pass
|
||||
30 | pass
|
||||
|
|
||||
= help: Parenthesize the `and` subexpression
|
||||
|
||||
ℹ Safe fix
|
||||
27 27 | pass
|
||||
28 28 |
|
||||
29 29 | a, b, c, d = 0, 1, 0, 2
|
||||
30 |-while a and b or c and d: # RUF021: => `(and b) or (c and d)`
|
||||
30 |+while a and b or (c and d): # RUF021: => `(and b) or (c and d)`
|
||||
31 31 | pass
|
||||
32 32 |
|
||||
33 33 | b, c, d, e = 2, 3, 0, 4
|
||||
|
||||
RUF021.py:35:44: RUF021 [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
RUF021.py:33:44: RUF021 Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
|
|
||||
33 | b, c, d, e = 2, 3, 0, 4
|
||||
34 | # RUF021: => `a or b or c or (d and e)`:
|
||||
35 | z = [a for a in range(5) if a or b or c or d and e]
|
||||
32 | b, c, d, e = 2, 3, 0, 4
|
||||
33 | z = [a for a in range(5) if a or b or c or d and e] # RUF021: => `a or b or c or (d and e)`
|
||||
| ^^^^^^^ RUF021
|
||||
36 |
|
||||
37 | a, b, c, d = 0, 1, 3, 0
|
||||
34 |
|
||||
35 | a, b, c, d = 0, 1, 3, 0
|
||||
|
|
||||
= help: Parenthesize the `and` subexpression
|
||||
|
||||
ℹ Safe fix
|
||||
32 32 |
|
||||
33 33 | b, c, d, e = 2, 3, 0, 4
|
||||
34 34 | # RUF021: => `a or b or c or (d and e)`:
|
||||
35 |-z = [a for a in range(5) if a or b or c or d and e]
|
||||
35 |+z = [a for a in range(5) if a or b or c or (d and e)]
|
||||
36 36 |
|
||||
37 37 | a, b, c, d = 0, 1, 3, 0
|
||||
38 38 | assert not a and b or c or d # RUF021: => `(not a and b) or c or d`
|
||||
|
||||
RUF021.py:38:8: RUF021 [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
RUF021.py:36:8: RUF021 Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
|
|
||||
37 | a, b, c, d = 0, 1, 3, 0
|
||||
38 | assert not a and b or c or d # RUF021: => `(not a and b) or c or d`
|
||||
35 | a, b, c, d = 0, 1, 3, 0
|
||||
36 | assert not a and b or c or d # RUF021: => `(not a and b) or c or d`
|
||||
| ^^^^^^^^^^^ RUF021
|
||||
39 |
|
||||
40 | if (not a) and b or c or d: # RUF021: => `((not a) and b) or c or d`
|
||||
37 |
|
||||
38 | if (not a) and b or c or d: # RUF021: => `((not a) and b) or c or d`
|
||||
|
|
||||
= help: Parenthesize the `and` subexpression
|
||||
|
||||
ℹ Safe fix
|
||||
35 35 | z = [a for a in range(5) if a or b or c or d and e]
|
||||
36 36 |
|
||||
37 37 | a, b, c, d = 0, 1, 3, 0
|
||||
38 |-assert not a and b or c or d # RUF021: => `(not a and b) or c or d`
|
||||
38 |+assert (not a and b) or c or d # RUF021: => `(not a and b) or c or d`
|
||||
39 39 |
|
||||
40 40 | if (not a) and b or c or d: # RUF021: => `((not a) and b) or c or d`
|
||||
41 41 | if (not a and b) or c or d: # OK
|
||||
|
||||
RUF021.py:40:4: RUF021 [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
RUF021.py:38:4: RUF021 Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
|
|
||||
38 | assert not a and b or c or d # RUF021: => `(not a and b) or c or d`
|
||||
39 |
|
||||
40 | if (not a) and b or c or d: # RUF021: => `((not a) and b) or c or d`
|
||||
36 | assert not a and b or c or d # RUF021: => `(not a and b) or c or d`
|
||||
37 |
|
||||
38 | if (not a) and b or c or d: # RUF021: => `((not a) and b) or c or d`
|
||||
| ^^^^^^^^^^^^^ RUF021
|
||||
41 | if (not a and b) or c or d: # OK
|
||||
42 | pass
|
||||
39 | if (not a and b) or c or d: # OK
|
||||
40 | pass
|
||||
|
|
||||
= help: Parenthesize the `and` subexpression
|
||||
|
||||
ℹ Safe fix
|
||||
37 37 | a, b, c, d = 0, 1, 3, 0
|
||||
38 38 | assert not a and b or c or d # RUF021: => `(not a and b) or c or d`
|
||||
39 39 |
|
||||
40 |-if (not a) and b or c or d: # RUF021: => `((not a) and b) or c or d`
|
||||
40 |+if ((not a) and b) or c or d: # RUF021: => `((not a) and b) or c or d`
|
||||
41 41 | if (not a and b) or c or d: # OK
|
||||
42 42 | pass
|
||||
43 43 |
|
||||
|
||||
RUF021.py:46:8: RUF021 [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
|
|
||||
44 | if (
|
||||
45 | some_reasonably_long_condition
|
||||
46 | or some_other_reasonably_long_condition
|
||||
| ________^
|
||||
47 | | and some_third_reasonably_long_condition
|
||||
| |____________________________________________^ RUF021
|
||||
48 | or some_fourth_reasonably_long_condition
|
||||
49 | and some_fifth_reasonably_long_condition
|
||||
|
|
||||
= help: Parenthesize the `and` subexpression
|
||||
|
||||
ℹ Safe fix
|
||||
43 43 |
|
||||
44 44 | if (
|
||||
45 45 | some_reasonably_long_condition
|
||||
46 |- or some_other_reasonably_long_condition
|
||||
47 |- and some_third_reasonably_long_condition
|
||||
46 |+ or (some_other_reasonably_long_condition
|
||||
47 |+ and some_third_reasonably_long_condition)
|
||||
48 48 | or some_fourth_reasonably_long_condition
|
||||
49 49 | and some_fifth_reasonably_long_condition
|
||||
50 50 | # a commment
|
||||
|
||||
RUF021.py:48:8: RUF021 [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
|
||||
|
|
||||
46 | or some_other_reasonably_long_condition
|
||||
47 | and some_third_reasonably_long_condition
|
||||
48 | or some_fourth_reasonably_long_condition
|
||||
| ________^
|
||||
49 | | and some_fifth_reasonably_long_condition
|
||||
50 | | # a commment
|
||||
51 | | and some_sixth_reasonably_long_condition
|
||||
52 | | and some_seventh_reasonably_long_condition
|
||||
| |______________________________________________^ RUF021
|
||||
53 | # another comment
|
||||
54 | or some_eighth_reasonably_long_condition
|
||||
|
|
||||
= help: Parenthesize the `and` subexpression
|
||||
|
||||
ℹ Safe fix
|
||||
45 45 | some_reasonably_long_condition
|
||||
46 46 | or some_other_reasonably_long_condition
|
||||
47 47 | and some_third_reasonably_long_condition
|
||||
48 |- or some_fourth_reasonably_long_condition
|
||||
48 |+ or (some_fourth_reasonably_long_condition
|
||||
49 49 | and some_fifth_reasonably_long_condition
|
||||
50 50 | # a commment
|
||||
51 51 | and some_sixth_reasonably_long_condition
|
||||
52 |- and some_seventh_reasonably_long_condition
|
||||
52 |+ and some_seventh_reasonably_long_condition)
|
||||
53 53 | # another comment
|
||||
54 54 | or some_eighth_reasonably_long_condition
|
||||
55 55 | ):
|
||||
|
||||
|
||||
|
||||
@@ -57,9 +57,6 @@ enum TypingTarget<'a> {
|
||||
/// A `typing.Annotated` type e.g., `Annotated[int, ...]`.
|
||||
Annotated(&'a Expr),
|
||||
|
||||
/// The `typing.Hashable` type.
|
||||
Hashable,
|
||||
|
||||
/// Special type used to represent an unknown type (and not a typing target)
|
||||
/// which could be a type alias.
|
||||
Unknown,
|
||||
@@ -124,10 +121,6 @@ impl<'a> TypingTarget<'a> {
|
||||
Some(TypingTarget::Any)
|
||||
} else if matches!(call_path.as_slice(), ["" | "builtins", "object"]) {
|
||||
Some(TypingTarget::Object)
|
||||
} else if semantic.match_typing_call_path(&call_path, "Hashable")
|
||||
|| matches!(call_path.as_slice(), ["collections", "abc", "Hashable"])
|
||||
{
|
||||
Some(TypingTarget::Hashable)
|
||||
} else if !is_known_type(&call_path, minor_version) {
|
||||
// If it's not a known type, we assume it's `Any`.
|
||||
Some(TypingTarget::Unknown)
|
||||
@@ -149,7 +142,6 @@ impl<'a> TypingTarget<'a> {
|
||||
match self {
|
||||
TypingTarget::None
|
||||
| TypingTarget::Optional(_)
|
||||
| TypingTarget::Hashable
|
||||
| TypingTarget::Any
|
||||
| TypingTarget::Object
|
||||
| TypingTarget::Unknown => true,
|
||||
@@ -199,7 +191,6 @@ impl<'a> TypingTarget<'a> {
|
||||
// `Literal` cannot contain `Any` as it's a dynamic value.
|
||||
TypingTarget::Literal(_)
|
||||
| TypingTarget::None
|
||||
| TypingTarget::Hashable
|
||||
| TypingTarget::Object
|
||||
| TypingTarget::Known
|
||||
| TypingTarget::Unknown => false,
|
||||
|
||||
@@ -149,7 +149,7 @@ fn f_strings() {
|
||||
|
||||
fn trace_preorder_visitation(source: &str) -> String {
|
||||
let tokens = lex(source, Mode::Module);
|
||||
let parsed = parse_tokens(tokens.collect(), source, Mode::Module).unwrap();
|
||||
let parsed = parse_tokens(tokens, source, Mode::Module).unwrap();
|
||||
|
||||
let mut visitor = RecordVisitor::default();
|
||||
visitor.visit_mod(&parsed);
|
||||
|
||||
@@ -160,7 +160,7 @@ fn f_strings() {
|
||||
|
||||
fn trace_visitation(source: &str) -> String {
|
||||
let tokens = lex(source, Mode::Module);
|
||||
let parsed = parse_tokens(tokens.collect(), source, Mode::Module).unwrap();
|
||||
let parsed = parse_tokens(tokens, source, Mode::Module).unwrap();
|
||||
|
||||
let mut visitor = RecordVisitor::default();
|
||||
walk_module(&mut visitor, &parsed);
|
||||
|
||||
@@ -11,7 +11,6 @@ class ClassWithSpaceParentheses:
|
||||
|
||||
|
||||
class ClassWithEmptyFunc(object):
|
||||
|
||||
def func_with_blank_parentheses():
|
||||
return 5
|
||||
|
||||
|
||||
@@ -65,25 +65,3 @@ def something():
|
||||
if flat
|
||||
else ValuesListIterable
|
||||
)
|
||||
|
||||
|
||||
def foo(wait: bool = True):
|
||||
# This comment is two
|
||||
# lines long
|
||||
|
||||
# This is only one
|
||||
time.sleep(1) if wait else None
|
||||
time.sleep(1) if wait else None
|
||||
|
||||
# With newline above
|
||||
time.sleep(1) if wait else None
|
||||
# Without newline above
|
||||
time.sleep(1) if wait else None
|
||||
|
||||
|
||||
a = "".join(
|
||||
(
|
||||
"", # comment
|
||||
"" if True else "",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -88,23 +88,3 @@ def something():
|
||||
if named
|
||||
else FlatValuesListIterable if flat else ValuesListIterable
|
||||
)
|
||||
|
||||
|
||||
def foo(wait: bool = True):
|
||||
# This comment is two
|
||||
# lines long
|
||||
|
||||
# This is only one
|
||||
time.sleep(1) if wait else None
|
||||
time.sleep(1) if wait else None
|
||||
|
||||
# With newline above
|
||||
time.sleep(1) if wait else None
|
||||
# Without newline above
|
||||
time.sleep(1) if wait else None
|
||||
|
||||
|
||||
a = "".join((
|
||||
"", # comment
|
||||
"" if True else "",
|
||||
))
|
||||
|
||||
@@ -5,7 +5,7 @@ def foo2(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parame
|
||||
def foo3(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
|
||||
# Adding some unformated code covering a wide range of syntaxes.
|
||||
# Adding some unformatted code covering a wide range of syntaxes.
|
||||
|
||||
if True:
|
||||
# Incorrectly indented prefix comments.
|
||||
|
||||
@@ -28,7 +28,7 @@ def foo3(
|
||||
|
||||
def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
|
||||
# Adding some unformated code covering a wide range of syntaxes.
|
||||
# Adding some unformatted code covering a wide range of syntaxes.
|
||||
|
||||
if True:
|
||||
# Incorrectly indented prefix comments.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""I am a very helpful module docstring.
|
||||
|
||||
With trailing spaces:
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
Ut enim ad minim veniam,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""I am a very helpful module docstring.
|
||||
|
||||
With trailing spaces:
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
Ut enim ad minim veniam,
|
||||
|
||||
@@ -16,15 +16,3 @@ if sys.version_info > (3, 7):
|
||||
def function_definition(self): ...
|
||||
assignment = 1
|
||||
def f2(self) -> str: ...
|
||||
|
||||
|
||||
class TopLevel:
|
||||
class Nested1:
|
||||
foo: int
|
||||
def bar(self): ...
|
||||
field = 1
|
||||
|
||||
class Nested2:
|
||||
def bar(self): ...
|
||||
foo: int
|
||||
field = 1
|
||||
|
||||
@@ -20,16 +20,3 @@ if sys.version_info > (3, 7):
|
||||
assignment = 1
|
||||
|
||||
def f2(self) -> str: ...
|
||||
|
||||
class TopLevel:
|
||||
class Nested1:
|
||||
foo: int
|
||||
def bar(self): ...
|
||||
|
||||
field = 1
|
||||
|
||||
class Nested2:
|
||||
def bar(self): ...
|
||||
foo: int
|
||||
|
||||
field = 1
|
||||
|
||||
@@ -19,7 +19,7 @@ z: (Short
|
||||
z: (int) = 2.3
|
||||
z: ((int)) = foo()
|
||||
|
||||
# In case I go for not enforcing parantheses, this might get improved at the same time
|
||||
# In case I go for not enforcing parentheses, this might get improved at the same time
|
||||
x = (
|
||||
z
|
||||
== 9999999999999999999999999999999999999999
|
||||
|
||||
@@ -28,7 +28,7 @@ z: Short | Short2 | Short3 | Short4 = 8
|
||||
z: int = 2.3
|
||||
z: int = foo()
|
||||
|
||||
# In case I go for not enforcing parantheses, this might get improved at the same time
|
||||
# In case I go for not enforcing parentheses, this might get improved at the same time
|
||||
x = (
|
||||
z
|
||||
== 9999999999999999999999999999999999999999
|
||||
|
||||
@@ -60,23 +60,3 @@ class Cls:
|
||||
def method(self):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
async def async_fn():
|
||||
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
@decorated
|
||||
async def async_fn():
|
||||
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
def top_level(
|
||||
a: int,
|
||||
b: str,
|
||||
) -> Whatever[Generic, Something]:
|
||||
|
||||
def nested(x: int) -> int:
|
||||
pass
|
||||
|
||||
@@ -57,25 +57,6 @@ def quux():
|
||||
|
||||
|
||||
class Cls:
|
||||
|
||||
def method(self):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
async def async_fn():
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
@decorated
|
||||
async def async_fn():
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
def top_level(
|
||||
a: int,
|
||||
b: str,
|
||||
) -> Whatever[Generic, Something]:
|
||||
|
||||
def nested(x: int) -> int:
|
||||
pass
|
||||
|
||||
@@ -56,14 +56,3 @@ if some_condition:
|
||||
...
|
||||
|
||||
if already_dummy: ...
|
||||
|
||||
class AsyncCls:
|
||||
async def async_method(self):
|
||||
...
|
||||
|
||||
async def async_function(self):
|
||||
...
|
||||
|
||||
@decorated
|
||||
async def async_function(self):
|
||||
...
|
||||
|
||||
@@ -59,14 +59,3 @@ if some_condition:
|
||||
|
||||
if already_dummy:
|
||||
...
|
||||
|
||||
|
||||
class AsyncCls:
|
||||
async def async_method(self): ...
|
||||
|
||||
|
||||
async def async_function(self): ...
|
||||
|
||||
|
||||
@decorated
|
||||
async def async_function(self): ...
|
||||
|
||||
@@ -79,7 +79,6 @@ def bar(a=1, b: bool = False):
|
||||
|
||||
|
||||
class Baz:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -27,7 +27,3 @@ class MultilineDocstringsAsWell:
|
||||
|
||||
and on so many lines...
|
||||
"""
|
||||
|
||||
class SingleQuotedDocstring:
|
||||
|
||||
"I'm a docstring but I don't even get triple quotes."
|
||||
|
||||
@@ -22,7 +22,3 @@ class MultilineDocstringsAsWell:
|
||||
|
||||
and on so many lines...
|
||||
"""
|
||||
|
||||
|
||||
class SingleQuotedDocstring:
|
||||
"I'm a docstring but I don't even get triple quotes."
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
x[(a := 0) :]
|
||||
x[: (a := 0)]
|
||||
x[(a := 0):]
|
||||
x[:(a := 0)]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"target_version": "py312"}
|
||||
@@ -1,10 +0,0 @@
|
||||
type A=int
|
||||
type Gen[T]=list[T]
|
||||
type Alias[T]=lambda: T
|
||||
type And[T]=T and T
|
||||
type IfElse[T]=T if T else T
|
||||
type One = int; type Another = str
|
||||
class X: type InClass = int
|
||||
|
||||
type = aliased
|
||||
print(type(42))
|
||||
@@ -1,15 +0,0 @@
|
||||
type A = int
|
||||
type Gen[T] = list[T]
|
||||
type Alias[T] = lambda: T
|
||||
type And[T] = T and T
|
||||
type IfElse[T] = T if T else T
|
||||
type One = int
|
||||
type Another = str
|
||||
|
||||
|
||||
class X:
|
||||
type InClass = int
|
||||
|
||||
|
||||
type = aliased
|
||||
print(type(42))
|
||||
@@ -96,6 +96,9 @@ IGNORE_LIST = [
|
||||
|
||||
# Uses a different output format
|
||||
"decorators.py",
|
||||
|
||||
# Ruff fails to parse because of a parser bug
|
||||
"type_aliases.py" # #8900 #8899
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# This file documents the deviations for formatting multiline strings with black.
|
||||
|
||||
# Black hugs the parentheses for `%` usages -> convert to fstring.
|
||||
# Can get unreadable if the arguments split
|
||||
# This could be solved by using `best_fitting` to try to format the arguments on a single
|
||||
# line. Let's consider adding this later.
|
||||
# ```python
|
||||
# call(
|
||||
# 3,
|
||||
# "dogsay",
|
||||
# textwrap.dedent(
|
||||
# """dove
|
||||
# coo""" % "cowabunga",
|
||||
# more,
|
||||
# and_more,
|
||||
# "aaaaaaa",
|
||||
# "bbbbbbbbb",
|
||||
# "cccccccc",
|
||||
# ),
|
||||
# )
|
||||
# ```
|
||||
call(3, "dogsay", textwrap.dedent("""dove
|
||||
coo""" % "cowabunga"))
|
||||
|
||||
# Black applies the hugging recursively. We don't (consistent with the hugging style).
|
||||
path.write_text(textwrap.dedent("""\
|
||||
A triple-quoted string
|
||||
actually leveraging the textwrap.dedent functionality
|
||||
that ends in a trailing newline,
|
||||
representing e.g. file contents.
|
||||
"""))
|
||||
|
||||
|
||||
|
||||
# Black avoids parenthesizing the following lambda. We could potentially support
|
||||
# this by changing `Lambda::needs_parentheses` to return `BestFit` but it causes
|
||||
# issues when the lambda has comments.
|
||||
# Let's keep this as a known deviation for now.
|
||||
generated_readme = lambda project_name: """
|
||||
{}
|
||||
|
||||
<Add content here!>
|
||||
""".strip().format(project_name)
|
||||
@@ -8,7 +8,7 @@ use clap::{command, Parser, ValueEnum};
|
||||
use ruff_formatter::SourceCode;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_index::tokens_and_ranges;
|
||||
use ruff_python_parser::{parse_tokens, AsMode};
|
||||
use ruff_python_parser::{parse_ok_tokens, AsMode};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::collect_comments;
|
||||
@@ -51,7 +51,7 @@ pub fn format_and_debug_print(source: &str, cli: &Cli, source_path: &Path) -> Re
|
||||
|
||||
// Parse the AST.
|
||||
let module =
|
||||
parse_tokens(tokens, source, source_type.as_mode()).context("Syntax error in input")?;
|
||||
parse_ok_tokens(tokens, source, source_type.as_mode()).context("Syntax error in input")?;
|
||||
|
||||
let options = PyFormatOptions::from_extension(source_path)
|
||||
.with_preview(if cli.preview {
|
||||
|
||||
@@ -102,12 +102,12 @@ use ruff_python_ast::Mod;
|
||||
use ruff_python_trivia::{CommentRanges, PythonWhitespace};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
pub(crate) use visitor::collect_comments;
|
||||
|
||||
use crate::comments::debug::{DebugComment, DebugComments};
|
||||
use crate::comments::map::{LeadingDanglingTrailing, MultiMap};
|
||||
use crate::comments::node_key::NodeRefEqualityKey;
|
||||
use crate::comments::visitor::{CommentsMapBuilder, CommentsVisitor};
|
||||
pub(crate) use visitor::collect_comments;
|
||||
|
||||
mod debug;
|
||||
pub(crate) mod format;
|
||||
@@ -563,7 +563,8 @@ mod tests {
|
||||
use ruff_formatter::SourceCode;
|
||||
use ruff_python_ast::{Mod, PySourceType};
|
||||
use ruff_python_index::tokens_and_ranges;
|
||||
use ruff_python_parser::{parse_tokens, AsMode};
|
||||
|
||||
use ruff_python_parser::{parse_ok_tokens, AsMode};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
|
||||
use crate::comments::Comments;
|
||||
@@ -580,7 +581,7 @@ mod tests {
|
||||
let source_type = PySourceType::Python;
|
||||
let (tokens, comment_ranges) =
|
||||
tokens_and_ranges(source, source_type).expect("Expect source to be valid Python");
|
||||
let parsed = parse_tokens(tokens, source, source_type.as_mode())
|
||||
let parsed = parse_ok_tokens(tokens, source, source_type.as_mode())
|
||||
.expect("Expect source to be valid Python");
|
||||
|
||||
CommentsTestCase {
|
||||
|
||||
@@ -394,12 +394,12 @@ impl Format<PyFormatContext<'_>> for BinaryLike<'_> {
|
||||
f,
|
||||
[
|
||||
operand.leading_binary_comments().map(leading_comments),
|
||||
leading_comments(comments.leading(string_constant)),
|
||||
leading_comments(comments.leading(&string_constant)),
|
||||
// Call `FormatStringContinuation` directly to avoid formatting
|
||||
// the implicitly concatenated string with the enclosing group
|
||||
// because the group is added by the binary like formatting.
|
||||
FormatStringContinuation::new(&string_constant),
|
||||
trailing_comments(comments.trailing(string_constant)),
|
||||
trailing_comments(comments.trailing(&string_constant)),
|
||||
operand.trailing_binary_comments().map(trailing_comments),
|
||||
line_suffix_boundary(),
|
||||
]
|
||||
@@ -413,12 +413,12 @@ impl Format<PyFormatContext<'_>> for BinaryLike<'_> {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
leading_comments(comments.leading(string_constant)),
|
||||
leading_comments(comments.leading(&string_constant)),
|
||||
// Call `FormatStringContinuation` directly to avoid formatting
|
||||
// the implicitly concatenated string with the enclosing group
|
||||
// because the group is added by the binary like formatting.
|
||||
FormatStringContinuation::new(&string_constant),
|
||||
trailing_comments(comments.trailing(string_constant)),
|
||||
trailing_comments(comments.trailing(&string_constant)),
|
||||
]
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ use ruff_python_ast::ExprBinOp;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::binary_like::BinaryLike;
|
||||
use crate::expression::expr_string_literal::is_multiline_string;
|
||||
use crate::expression::has_parentheses;
|
||||
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
|
||||
use crate::prelude::*;
|
||||
use crate::string::AnyString;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatExprBinOp;
|
||||
@@ -35,13 +35,13 @@ impl NeedsParentheses for ExprBinOp {
|
||||
) -> OptionalParentheses {
|
||||
if parent.is_expr_await() {
|
||||
OptionalParentheses::Always
|
||||
} else if let Some(string) = AnyString::from_expression(&self.left) {
|
||||
} else if let Some(literal_expr) = self.left.as_literal_expr() {
|
||||
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
|
||||
if !string.is_implicit_concatenated()
|
||||
&& string.is_multiline(context.source())
|
||||
if !literal_expr.is_implicit_concatenated()
|
||||
&& is_multiline_string(literal_expr.into(), context.source())
|
||||
&& has_parentheses(&self.right, context).is_some()
|
||||
&& !context.comments().has_dangling(self)
|
||||
&& !context.comments().has(string)
|
||||
&& !context.comments().has(literal_expr)
|
||||
&& !context.comments().has(self.right.as_ref())
|
||||
{
|
||||
OptionalParentheses::Never
|
||||
|
||||
@@ -2,6 +2,7 @@ use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::ExprBytesLiteral;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::expr_string_literal::is_multiline_string;
|
||||
use crate::expression::parentheses::{
|
||||
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
|
||||
};
|
||||
@@ -40,7 +41,7 @@ impl NeedsParentheses for ExprBytesLiteral {
|
||||
) -> OptionalParentheses {
|
||||
if self.value.is_implicit_concatenated() {
|
||||
OptionalParentheses::Multiline
|
||||
} else if AnyString::Bytes(self).is_multiline(context.source()) {
|
||||
} else if is_multiline_string(self.into(), context.source()) {
|
||||
OptionalParentheses::Never
|
||||
} else {
|
||||
OptionalParentheses::BestFit
|
||||
|
||||
@@ -4,10 +4,10 @@ use ruff_python_ast::{CmpOp, ExprCompare};
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::binary_like::BinaryLike;
|
||||
use crate::expression::expr_string_literal::is_multiline_string;
|
||||
use crate::expression::has_parentheses;
|
||||
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
|
||||
use crate::prelude::*;
|
||||
use crate::string::AnyString;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatExprCompare;
|
||||
@@ -37,11 +37,11 @@ impl NeedsParentheses for ExprCompare {
|
||||
) -> OptionalParentheses {
|
||||
if parent.is_expr_await() {
|
||||
OptionalParentheses::Always
|
||||
} else if let Some(string) = AnyString::from_expression(&self.left) {
|
||||
} else if let Some(literal_expr) = self.left.as_literal_expr() {
|
||||
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
|
||||
if !string.is_implicit_concatenated()
|
||||
&& string.is_multiline(context.source())
|
||||
&& !context.comments().has(string)
|
||||
if !literal_expr.is_implicit_concatenated()
|
||||
&& is_multiline_string(literal_expr.into(), context.source())
|
||||
&& !context.comments().has(literal_expr)
|
||||
&& self.comparators.first().is_some_and(|right| {
|
||||
has_parentheses(right, context).is_some() && !context.comments().has(right)
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use memchr::memchr2;
|
||||
|
||||
use ruff_python_ast::{AnyNodeRef, ExprFString};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -48,10 +50,10 @@ impl NeedsParentheses for ExprFString {
|
||||
) -> OptionalParentheses {
|
||||
if self.value.is_implicit_concatenated() {
|
||||
OptionalParentheses::Multiline
|
||||
} else if AnyString::FString(self).is_multiline(context.source()) {
|
||||
OptionalParentheses::Never
|
||||
} else {
|
||||
} else if memchr2(b'\n', b'\r', context.source()[self.range].as_bytes()).is_none() {
|
||||
OptionalParentheses::BestFit
|
||||
} else {
|
||||
OptionalParentheses::Never
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use ruff_formatter::FormatRuleWithOptions;
|
||||
use ruff_python_ast::{AnyNodeRef, ExprStringLiteral};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::parentheses::{
|
||||
@@ -7,7 +8,7 @@ use crate::expression::parentheses::{
|
||||
};
|
||||
use crate::other::string_literal::{FormatStringLiteral, StringLiteralKind};
|
||||
use crate::prelude::*;
|
||||
use crate::string::{AnyString, FormatStringContinuation};
|
||||
use crate::string::{AnyString, FormatStringContinuation, StringPrefix, StringQuotes};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatExprStringLiteral {
|
||||
@@ -79,10 +80,24 @@ impl NeedsParentheses for ExprStringLiteral {
|
||||
) -> OptionalParentheses {
|
||||
if self.value.is_implicit_concatenated() {
|
||||
OptionalParentheses::Multiline
|
||||
} else if AnyString::String(self).is_multiline(context.source()) {
|
||||
} else if is_multiline_string(self.into(), context.source()) {
|
||||
OptionalParentheses::Never
|
||||
} else {
|
||||
OptionalParentheses::BestFit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_multiline_string(expr: AnyNodeRef, source: &str) -> bool {
|
||||
if expr.is_expr_string_literal() || expr.is_expr_bytes_literal() {
|
||||
let contents = &source[expr.range()];
|
||||
let prefix = StringPrefix::parse(contents);
|
||||
let quotes =
|
||||
StringQuotes::parse(&contents[TextRange::new(prefix.text_len(), contents.text_len())]);
|
||||
|
||||
quotes.is_some_and(StringQuotes::is_triple)
|
||||
&& memchr::memchr2(b'\n', b'\r', contents.as_bytes()).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,11 @@ use crate::context::{NodeLevel, WithNodeLevel};
|
||||
use crate::expression::expr_generator_exp::is_generator_parenthesized;
|
||||
use crate::expression::expr_tuple::is_tuple_parenthesized;
|
||||
use crate::expression::parentheses::{
|
||||
is_expression_parenthesized, optional_parentheses, parenthesized, HuggingStyle,
|
||||
NeedsParentheses, OptionalParentheses, Parentheses, Parenthesize,
|
||||
is_expression_parenthesized, optional_parentheses, parenthesized, NeedsParentheses,
|
||||
OptionalParentheses, Parentheses, Parenthesize,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::preview::{
|
||||
is_hug_parens_with_braces_and_square_brackets_enabled, is_multiline_string_handling_enabled,
|
||||
};
|
||||
use crate::string::AnyString;
|
||||
use crate::preview::is_hug_parens_with_braces_and_square_brackets_enabled;
|
||||
|
||||
mod binary_like;
|
||||
pub(crate) mod expr_attribute;
|
||||
@@ -129,7 +126,7 @@ impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
|
||||
let node_comments = comments.leading_dangling_trailing(expression);
|
||||
if !node_comments.has_leading() && !node_comments.has_trailing() {
|
||||
parenthesized("(", &format_expr, ")")
|
||||
.with_hugging(is_expression_huggable(expression, f.context()))
|
||||
.with_indent(!is_expression_huggable(expression, f.context()))
|
||||
.fmt(f)
|
||||
} else {
|
||||
format_with_parentheses_comments(expression, &node_comments, f)
|
||||
@@ -447,7 +444,7 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
|
||||
OptionalParentheses::Never => match parenthesize {
|
||||
Parenthesize::IfBreaksOrIfRequired => {
|
||||
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
|
||||
.with_indent(is_expression_huggable(expression, f.context()).is_none())
|
||||
.with_indent(!is_expression_huggable(expression, f.context()))
|
||||
.fmt(f)
|
||||
}
|
||||
|
||||
@@ -1087,7 +1084,7 @@ pub(crate) fn has_own_parentheses(
|
||||
}
|
||||
|
||||
/// Returns `true` if the expression can hug directly to enclosing parentheses, as in Black's
|
||||
/// `hug_parens_with_braces_and_square_brackets` or `multiline_string_handling` preview styles behavior.
|
||||
/// `hug_parens_with_braces_and_square_brackets` preview style behavior.
|
||||
///
|
||||
/// For example, in preview style, given:
|
||||
/// ```python
|
||||
@@ -1113,10 +1110,11 @@ pub(crate) fn has_own_parentheses(
|
||||
/// ]
|
||||
/// )
|
||||
/// ```
|
||||
pub(crate) fn is_expression_huggable(
|
||||
expr: &Expr,
|
||||
context: &PyFormatContext,
|
||||
) -> Option<HuggingStyle> {
|
||||
pub(crate) fn is_expression_huggable(expr: &Expr, context: &PyFormatContext) -> bool {
|
||||
if !is_hug_parens_with_braces_and_square_brackets_enabled(context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
match expr {
|
||||
Expr::Tuple(_)
|
||||
| Expr::List(_)
|
||||
@@ -1124,14 +1122,18 @@ pub(crate) fn is_expression_huggable(
|
||||
| Expr::Dict(_)
|
||||
| Expr::ListComp(_)
|
||||
| Expr::SetComp(_)
|
||||
| Expr::DictComp(_) => is_hug_parens_with_braces_and_square_brackets_enabled(context)
|
||||
.then_some(HuggingStyle::Always),
|
||||
| Expr::DictComp(_) => true,
|
||||
|
||||
Expr::Starred(ast::ExprStarred { value, .. }) => is_expression_huggable(value, context),
|
||||
|
||||
Expr::StringLiteral(string) => is_huggable_string(AnyString::String(string), context),
|
||||
Expr::BytesLiteral(bytes) => is_huggable_string(AnyString::Bytes(bytes), context),
|
||||
Expr::FString(fstring) => is_huggable_string(AnyString::FString(fstring), context),
|
||||
Expr::Starred(ast::ExprStarred { value, .. }) => matches!(
|
||||
value.as_ref(),
|
||||
Expr::Tuple(_)
|
||||
| Expr::List(_)
|
||||
| Expr::Set(_)
|
||||
| Expr::Dict(_)
|
||||
| Expr::ListComp(_)
|
||||
| Expr::SetComp(_)
|
||||
| Expr::DictComp(_)
|
||||
),
|
||||
|
||||
Expr::BoolOp(_)
|
||||
| Expr::NamedExpr(_)
|
||||
@@ -1145,28 +1147,18 @@ pub(crate) fn is_expression_huggable(
|
||||
| Expr::YieldFrom(_)
|
||||
| Expr::Compare(_)
|
||||
| Expr::Call(_)
|
||||
| Expr::FString(_)
|
||||
| Expr::Attribute(_)
|
||||
| Expr::Subscript(_)
|
||||
| Expr::Name(_)
|
||||
| Expr::Slice(_)
|
||||
| Expr::IpyEscapeCommand(_)
|
||||
| Expr::StringLiteral(_)
|
||||
| Expr::BytesLiteral(_)
|
||||
| Expr::NumberLiteral(_)
|
||||
| Expr::BooleanLiteral(_)
|
||||
| Expr::NoneLiteral(_)
|
||||
| Expr::EllipsisLiteral(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `string` is a multiline string that is not implicitly concatenated.
|
||||
fn is_huggable_string(string: AnyString, context: &PyFormatContext) -> Option<HuggingStyle> {
|
||||
if !is_multiline_string_handling_enabled(context) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !string.is_implicit_concatenated() && string.is_multiline(context.source()) {
|
||||
Some(HuggingStyle::IfFirstLineFits)
|
||||
} else {
|
||||
None
|
||||
| Expr::EllipsisLiteral(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ where
|
||||
FormatParenthesized {
|
||||
left,
|
||||
comments: &[],
|
||||
hug: None,
|
||||
indent: true,
|
||||
content: Argument::new(content),
|
||||
right,
|
||||
}
|
||||
@@ -135,7 +135,7 @@ where
|
||||
pub(crate) struct FormatParenthesized<'content, 'ast> {
|
||||
left: &'static str,
|
||||
comments: &'content [SourceComment],
|
||||
hug: Option<HuggingStyle>,
|
||||
indent: bool,
|
||||
content: Argument<'content, PyFormatContext<'ast>>,
|
||||
right: &'static str,
|
||||
}
|
||||
@@ -158,11 +158,8 @@ impl<'content, 'ast> FormatParenthesized<'content, 'ast> {
|
||||
}
|
||||
|
||||
/// Whether to indent the content within the parentheses.
|
||||
pub(crate) fn with_hugging(
|
||||
self,
|
||||
hug: Option<HuggingStyle>,
|
||||
) -> FormatParenthesized<'content, 'ast> {
|
||||
FormatParenthesized { hug, ..self }
|
||||
pub(crate) fn with_indent(self, indent: bool) -> FormatParenthesized<'content, 'ast> {
|
||||
FormatParenthesized { indent, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,41 +167,17 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatParenthesized<'_, 'ast> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> {
|
||||
let current_level = f.context().node_level();
|
||||
|
||||
let indented = format_with(|f| {
|
||||
let content = Arguments::from(&self.content);
|
||||
if self.comments.is_empty() {
|
||||
match self.hug {
|
||||
None => group(&soft_block_indent(&content)).fmt(f),
|
||||
Some(HuggingStyle::Always) => content.fmt(f),
|
||||
Some(HuggingStyle::IfFirstLineFits) => {
|
||||
// It's not immediately obvious how the below IR works to only indent the content if the first line exceeds the configured line width.
|
||||
// The trick is the first group that doesn't wrap `self.content`.
|
||||
// * The group doesn't wrap `self.content` because we need to assume that `self.content`
|
||||
// contains a hard line break and hard-line-breaks always expand the enclosing group.
|
||||
// * The printer decides that a group fits if its content (in this case a `soft_line_break` that has a width of 0 and is guaranteed to fit)
|
||||
// and the content coming after the group in expanded mode (`self.content`) fits on the line.
|
||||
// The content coming after fits if the content up to the first soft or hard line break (or the end of the document) fits.
|
||||
//
|
||||
// This happens to be right what we want. The first group should add an indent and a soft line break if the content of `self.content`
|
||||
// up to the first line break exceeds the configured line length, but not otherwise.
|
||||
let indented = f.group_id("indented_content");
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
group(&indent(&soft_line_break())).with_group_id(Some(indented)),
|
||||
indent_if_group_breaks(&content, indented),
|
||||
if_group_breaks(&soft_line_break()).with_group_id(Some(indented))
|
||||
]
|
||||
)
|
||||
}
|
||||
let content = format_with(|f| {
|
||||
group(&format_with(|f| {
|
||||
dangling_open_parenthesis_comments(self.comments).fmt(f)?;
|
||||
if self.indent || !self.comments.is_empty() {
|
||||
soft_block_indent(&Arguments::from(&self.content)).fmt(f)?;
|
||||
} else {
|
||||
Arguments::from(&self.content).fmt(f)?;
|
||||
}
|
||||
} else {
|
||||
group(&format_args![
|
||||
dangling_open_parenthesis_comments(self.comments),
|
||||
soft_block_indent(&content),
|
||||
])
|
||||
.fmt(f)
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
.fmt(f)
|
||||
});
|
||||
|
||||
let inner = format_with(|f| {
|
||||
@@ -213,12 +186,12 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatParenthesized<'_, 'ast> {
|
||||
// This ensures that expanding this parenthesized expression does not expand the optional parentheses group.
|
||||
write!(
|
||||
f,
|
||||
[fits_expanded(&indented)
|
||||
[fits_expanded(&content)
|
||||
.with_condition(Some(Condition::if_group_fits_on_line(group_id)))]
|
||||
)
|
||||
} else {
|
||||
// It's not necessary to wrap the content if it is not inside of an optional_parentheses group.
|
||||
indented.fmt(f)
|
||||
content.fmt(f)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -228,20 +201,6 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatParenthesized<'_, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum HuggingStyle {
|
||||
/// Always hug the content (never indent).
|
||||
Always,
|
||||
|
||||
/// Hug the content if the content up to the first line break fits into the configured line length. Otherwise indent the content.
|
||||
///
|
||||
/// This is different from [`HuggingStyle::Always`] in that it doesn't indent if the content contains a hard line break, and the content up to that hard line break fits into the configured line length.
|
||||
///
|
||||
/// This style is used for formatting multiline strings that, by definition, always break. The idea is to
|
||||
/// only hug a multiline string if its content up to the first line breaks exceeds the configured line length.
|
||||
IfFirstLineFits,
|
||||
}
|
||||
|
||||
/// Wraps an expression in parentheses only if it still does not fit after expanding all expressions that start or end with
|
||||
/// a parentheses (`()`, `[]`, `{}`).
|
||||
pub(crate) fn optional_parentheses<'content, 'ast, Content>(
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_formatter::{format, FormatError, Formatted, PrintError, Printed, Source
|
||||
use ruff_python_ast::AstNode;
|
||||
use ruff_python_ast::Mod;
|
||||
use ruff_python_index::tokens_and_ranges;
|
||||
use ruff_python_parser::{parse_tokens, AsMode, ParseError, ParseErrorType};
|
||||
use ruff_python_parser::{parse_ok_tokens, AsMode, ParseError, ParseErrorType};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
@@ -126,7 +126,7 @@ pub fn format_module_source(
|
||||
offset: err.location,
|
||||
error: ParseErrorType::Lexical(err.error),
|
||||
})?;
|
||||
let module = parse_tokens(tokens, source, source_type.as_mode())?;
|
||||
let module = parse_ok_tokens(tokens, source, source_type.as_mode())?;
|
||||
let formatted = format_module_ast(&module, &comment_ranges, source, options)?;
|
||||
Ok(formatted.print()?)
|
||||
}
|
||||
@@ -169,7 +169,7 @@ mod tests {
|
||||
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_index::tokens_and_ranges;
|
||||
use ruff_python_parser::{parse_tokens, AsMode};
|
||||
use ruff_python_parser::{parse_ok_tokens, AsMode};
|
||||
|
||||
use crate::{format_module_ast, format_module_source, PyFormatOptions};
|
||||
|
||||
@@ -213,7 +213,7 @@ def main() -> None:
|
||||
|
||||
// Parse the AST.
|
||||
let source_path = "code_inline.py";
|
||||
let module = parse_tokens(tokens, source, source_type.as_mode()).unwrap();
|
||||
let module = parse_ok_tokens(tokens, source, source_type.as_mode()).unwrap();
|
||||
let options = PyFormatOptions::from_extension(Path::new(source_path));
|
||||
let formatted = format_module_ast(&module, &comment_ranges, source, options).unwrap();
|
||||
|
||||
|
||||
@@ -6,11 +6,10 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::expr_generator_exp::GeneratorExpParentheses;
|
||||
use crate::expression::is_expression_huggable;
|
||||
use crate::expression::parentheses::{
|
||||
empty_parenthesized, parenthesized, HuggingStyle, Parentheses,
|
||||
};
|
||||
use crate::expression::parentheses::{empty_parenthesized, parenthesized, Parentheses};
|
||||
use crate::other::commas;
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_hug_parens_with_braces_and_square_brackets_enabled;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatArguments;
|
||||
@@ -108,7 +107,7 @@ impl FormatNodeRule<Arguments> for FormatArguments {
|
||||
// )
|
||||
// ```
|
||||
parenthesized("(", &group(&all_arguments), ")")
|
||||
.with_hugging(is_arguments_huggable(item, f.context()))
|
||||
.with_indent(!is_argument_huggable(item, f.context()))
|
||||
.with_dangling_comments(dangling_comments)
|
||||
]
|
||||
)
|
||||
@@ -178,23 +177,29 @@ fn is_single_argument_parenthesized(argument: &Expr, call_end: TextSize, source:
|
||||
///
|
||||
/// Hugging should only be applied to single-argument collections, like lists, or starred versions
|
||||
/// of those collections.
|
||||
fn is_arguments_huggable(item: &Arguments, context: &PyFormatContext) -> Option<HuggingStyle> {
|
||||
fn is_argument_huggable(item: &Arguments, context: &PyFormatContext) -> bool {
|
||||
if !is_hug_parens_with_braces_and_square_brackets_enabled(context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the lone argument or `**kwargs` keyword.
|
||||
let arg = match (item.args.as_slice(), item.keywords.as_slice()) {
|
||||
([arg], []) => arg,
|
||||
([], [keyword]) if keyword.arg.is_none() && !context.comments().has(keyword) => {
|
||||
&keyword.value
|
||||
}
|
||||
_ => return None,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
// If the expression itself isn't huggable, then we can't hug it.
|
||||
let hugging_style = is_expression_huggable(arg, context)?;
|
||||
if !is_expression_huggable(arg, context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the expression has leading or trailing comments, then we can't hug it.
|
||||
let comments = context.comments().leading_dangling_trailing(arg);
|
||||
if comments.has_leading() || comments.has_trailing() {
|
||||
return None;
|
||||
return false;
|
||||
}
|
||||
|
||||
let options = context.options();
|
||||
@@ -203,8 +208,8 @@ fn is_arguments_huggable(item: &Arguments, context: &PyFormatContext) -> Option<
|
||||
if options.magic_trailing_comma().is_respect()
|
||||
&& commas::has_magic_trailing_comma(TextRange::new(arg.end(), item.end()), options, context)
|
||||
{
|
||||
return None;
|
||||
return false;
|
||||
}
|
||||
|
||||
Some(hugging_style)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -62,8 +62,3 @@ pub(crate) const fn is_dummy_implementations_enabled(context: &PyFormatContext)
|
||||
pub(crate) const fn is_hex_codes_in_unicode_sequences_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
/// Returns `true` if the [`multiline_string_handling`](https://github.com/astral-sh/ruff/issues/8896) preview style is enabled.
|
||||
pub(crate) const fn is_multiline_string_handling_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
@@ -1516,7 +1516,7 @@ fn docstring_format_source(
|
||||
let source_type = options.source_type();
|
||||
let (tokens, comment_ranges) =
|
||||
ruff_python_index::tokens_and_ranges(source, source_type).map_err(ParseError::from)?;
|
||||
let module = ruff_python_parser::parse_tokens(tokens, source, source_type.as_mode())?;
|
||||
let module = ruff_python_parser::parse_ok_tokens(tokens, source, source_type.as_mode())?;
|
||||
let source_code = ruff_formatter::SourceCode::new(source);
|
||||
let comments = crate::Comments::from_ast(&module, source_code, &comment_ranges);
|
||||
let locator = Locator::new(source);
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use std::borrow::Cow;
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use memchr::memchr2;
|
||||
|
||||
use ruff_formatter::{format_args, write};
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::{
|
||||
self as ast, Expr, ExprBytesLiteral, ExprFString, ExprStringLiteral, ExpressionRef,
|
||||
};
|
||||
use ruff_python_ast::{AnyNodeRef, StringLiteral};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
@@ -31,7 +29,7 @@ pub(crate) enum Quoting {
|
||||
|
||||
/// Represents any kind of string expression. This could be either a string,
|
||||
/// bytes or f-string.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum AnyString<'a> {
|
||||
String(&'a ExprStringLiteral),
|
||||
Bytes(&'a ExprBytesLiteral),
|
||||
@@ -52,7 +50,7 @@ impl<'a> AnyString<'a> {
|
||||
}
|
||||
|
||||
/// Returns `true` if the string is implicitly concatenated.
|
||||
pub(crate) fn is_implicit_concatenated(self) -> bool {
|
||||
pub(crate) fn is_implicit_concatenated(&self) -> bool {
|
||||
match self {
|
||||
Self::String(ExprStringLiteral { value, .. }) => value.is_implicit_concatenated(),
|
||||
Self::Bytes(ExprBytesLiteral { value, .. }) => value.is_implicit_concatenated(),
|
||||
@@ -61,7 +59,7 @@ impl<'a> AnyString<'a> {
|
||||
}
|
||||
|
||||
/// Returns the quoting to be used for this string.
|
||||
fn quoting(self, locator: &Locator<'_>) -> Quoting {
|
||||
fn quoting(&self, locator: &Locator<'_>) -> Quoting {
|
||||
match self {
|
||||
Self::String(_) | Self::Bytes(_) => Quoting::CanChange,
|
||||
Self::FString(f_string) => f_string_quoting(f_string, locator),
|
||||
@@ -69,33 +67,31 @@ impl<'a> AnyString<'a> {
|
||||
}
|
||||
|
||||
/// Returns a vector of all the [`AnyStringPart`] of this string.
|
||||
fn parts(self, quoting: Quoting) -> AnyStringPartsIter<'a> {
|
||||
fn parts(&self, quoting: Quoting) -> Vec<AnyStringPart<'a>> {
|
||||
match self {
|
||||
Self::String(ExprStringLiteral { value, .. }) => {
|
||||
AnyStringPartsIter::String(value.iter())
|
||||
}
|
||||
Self::Bytes(ExprBytesLiteral { value, .. }) => AnyStringPartsIter::Bytes(value.iter()),
|
||||
Self::FString(ExprFString { value, .. }) => {
|
||||
AnyStringPartsIter::FString(value.iter(), quoting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_multiline(self, source: &str) -> bool {
|
||||
match self {
|
||||
AnyString::String(_) | AnyString::Bytes(_) => {
|
||||
let contents = &source[self.range()];
|
||||
let prefix = StringPrefix::parse(contents);
|
||||
let quotes = StringQuotes::parse(
|
||||
&contents[TextRange::new(prefix.text_len(), contents.text_len())],
|
||||
);
|
||||
|
||||
quotes.is_some_and(StringQuotes::is_triple)
|
||||
&& memchr2(b'\n', b'\r', contents.as_bytes()).is_some()
|
||||
}
|
||||
AnyString::FString(fstring) => {
|
||||
memchr2(b'\n', b'\r', source[fstring.range].as_bytes()).is_some()
|
||||
Self::String(ExprStringLiteral { value, .. }) => value
|
||||
.iter()
|
||||
.map(|part| AnyStringPart::String {
|
||||
part,
|
||||
layout: StringLiteralKind::String,
|
||||
})
|
||||
.collect(),
|
||||
Self::Bytes(ExprBytesLiteral { value, .. }) => {
|
||||
value.iter().map(AnyStringPart::Bytes).collect()
|
||||
}
|
||||
Self::FString(ExprFString { value, .. }) => value
|
||||
.iter()
|
||||
.map(|f_string_part| match f_string_part {
|
||||
ast::FStringPart::Literal(string_literal) => AnyStringPart::String {
|
||||
part: string_literal,
|
||||
layout: StringLiteralKind::InImplicitlyConcatenatedFString(quoting),
|
||||
},
|
||||
ast::FStringPart::FString(f_string) => AnyStringPart::FString {
|
||||
part: f_string,
|
||||
quoting,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,12 +116,6 @@ impl<'a> From<&AnyString<'a>> for AnyNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<AnyString<'a>> for AnyNodeRef<'a> {
|
||||
fn from(value: AnyString<'a>) -> Self {
|
||||
AnyNodeRef::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&AnyString<'a>> for ExpressionRef<'a> {
|
||||
fn from(value: &AnyString<'a>) -> Self {
|
||||
match value {
|
||||
@@ -136,46 +126,6 @@ impl<'a> From<&AnyString<'a>> for ExpressionRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
enum AnyStringPartsIter<'a> {
|
||||
String(std::slice::Iter<'a, StringLiteral>),
|
||||
Bytes(std::slice::Iter<'a, ast::BytesLiteral>),
|
||||
FString(std::slice::Iter<'a, ast::FStringPart>, Quoting),
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AnyStringPartsIter<'a> {
|
||||
type Item = AnyStringPart<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let part = match self {
|
||||
Self::String(inner) => {
|
||||
let part = inner.next()?;
|
||||
AnyStringPart::String {
|
||||
part,
|
||||
layout: StringLiteralKind::String,
|
||||
}
|
||||
}
|
||||
Self::Bytes(inner) => AnyStringPart::Bytes(inner.next()?),
|
||||
Self::FString(inner, quoting) => {
|
||||
let part = inner.next()?;
|
||||
match part {
|
||||
ast::FStringPart::Literal(string_literal) => AnyStringPart::String {
|
||||
part: string_literal,
|
||||
layout: StringLiteralKind::InImplicitlyConcatenatedFString(*quoting),
|
||||
},
|
||||
ast::FStringPart::FString(f_string) => AnyStringPart::FString {
|
||||
part: f_string,
|
||||
quoting: *quoting,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(part)
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for AnyStringPartsIter<'_> {}
|
||||
|
||||
/// Represents any kind of string which is part of an implicitly concatenated
|
||||
/// string. This could be either a string, bytes or f-string.
|
||||
///
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/class_blank_parentheses.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
class SimpleClassWithBlankParentheses():
|
||||
pass
|
||||
class ClassWithSpaceParentheses ( ):
|
||||
first_test_data = 90
|
||||
second_test_data = 100
|
||||
def test_func(self):
|
||||
return None
|
||||
class ClassWithEmptyFunc(object):
|
||||
|
||||
def func_with_blank_parentheses():
|
||||
return 5
|
||||
|
||||
|
||||
def public_func_with_blank_parentheses():
|
||||
return None
|
||||
def class_under_the_func_with_blank_parentheses():
|
||||
class InsideFunc():
|
||||
pass
|
||||
class NormalClass (
|
||||
):
|
||||
def func_for_testing(self, first, second):
|
||||
sum = first + second
|
||||
return sum
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
|
||||
class ClassWithEmptyFunc(object):
|
||||
-
|
||||
def func_with_blank_parentheses():
|
||||
return 5
|
||||
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
class SimpleClassWithBlankParentheses:
|
||||
pass
|
||||
|
||||
|
||||
class ClassWithSpaceParentheses:
|
||||
first_test_data = 90
|
||||
second_test_data = 100
|
||||
|
||||
def test_func(self):
|
||||
return None
|
||||
|
||||
|
||||
class ClassWithEmptyFunc(object):
|
||||
def func_with_blank_parentheses():
|
||||
return 5
|
||||
|
||||
|
||||
def public_func_with_blank_parentheses():
|
||||
return None
|
||||
|
||||
|
||||
def class_under_the_func_with_blank_parentheses():
|
||||
class InsideFunc:
|
||||
pass
|
||||
|
||||
|
||||
class NormalClass:
|
||||
def func_for_testing(self, first, second):
|
||||
sum = first + second
|
||||
return sum
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
class SimpleClassWithBlankParentheses:
|
||||
pass
|
||||
|
||||
|
||||
class ClassWithSpaceParentheses:
|
||||
first_test_data = 90
|
||||
second_test_data = 100
|
||||
|
||||
def test_func(self):
|
||||
return None
|
||||
|
||||
|
||||
class ClassWithEmptyFunc(object):
|
||||
|
||||
def func_with_blank_parentheses():
|
||||
return 5
|
||||
|
||||
|
||||
def public_func_with_blank_parentheses():
|
||||
return None
|
||||
|
||||
|
||||
def class_under_the_func_with_blank_parentheses():
|
||||
class InsideFunc:
|
||||
pass
|
||||
|
||||
|
||||
class NormalClass:
|
||||
def func_for_testing(self, first, second):
|
||||
sum = first + second
|
||||
return sum
|
||||
```
|
||||
|
||||
|
||||
@@ -72,28 +72,6 @@ def something():
|
||||
if flat
|
||||
else ValuesListIterable
|
||||
)
|
||||
|
||||
|
||||
def foo(wait: bool = True):
|
||||
# This comment is two
|
||||
# lines long
|
||||
|
||||
# This is only one
|
||||
time.sleep(1) if wait else None
|
||||
time.sleep(1) if wait else None
|
||||
|
||||
# With newline above
|
||||
time.sleep(1) if wait else None
|
||||
# Without newline above
|
||||
time.sleep(1) if wait else None
|
||||
|
||||
|
||||
a = "".join(
|
||||
(
|
||||
"", # comment
|
||||
"" if True else "",
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
@@ -158,7 +136,7 @@ a = "".join(
|
||||
for some_boolean_variable in some_iterable
|
||||
)
|
||||
|
||||
@@ -86,7 +78,9 @@
|
||||
@@ -86,5 +78,7 @@
|
||||
clone._iterable_class = (
|
||||
NamedValuesListIterable
|
||||
if named
|
||||
@@ -167,8 +145,6 @@ a = "".join(
|
||||
+ if flat
|
||||
+ else ValuesListIterable
|
||||
)
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
@@ -258,26 +234,6 @@ def something():
|
||||
if flat
|
||||
else ValuesListIterable
|
||||
)
|
||||
|
||||
|
||||
def foo(wait: bool = True):
|
||||
# This comment is two
|
||||
# lines long
|
||||
|
||||
# This is only one
|
||||
time.sleep(1) if wait else None
|
||||
time.sleep(1) if wait else None
|
||||
|
||||
# With newline above
|
||||
time.sleep(1) if wait else None
|
||||
# Without newline above
|
||||
time.sleep(1) if wait else None
|
||||
|
||||
|
||||
a = "".join((
|
||||
"", # comment
|
||||
"" if True else "",
|
||||
))
|
||||
```
|
||||
|
||||
## Black Output
|
||||
@@ -373,26 +329,6 @@ def something():
|
||||
if named
|
||||
else FlatValuesListIterable if flat else ValuesListIterable
|
||||
)
|
||||
|
||||
|
||||
def foo(wait: bool = True):
|
||||
# This comment is two
|
||||
# lines long
|
||||
|
||||
# This is only one
|
||||
time.sleep(1) if wait else None
|
||||
time.sleep(1) if wait else None
|
||||
|
||||
# With newline above
|
||||
time.sleep(1) if wait else None
|
||||
# Without newline above
|
||||
time.sleep(1) if wait else None
|
||||
|
||||
|
||||
a = "".join((
|
||||
"", # comment
|
||||
"" if True else "",
|
||||
))
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ def foo2(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parame
|
||||
def foo3(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
|
||||
# Adding some unformated code covering a wide range of syntaxes.
|
||||
# Adding some unformatted code covering a wide range of syntaxes.
|
||||
|
||||
if True:
|
||||
# Incorrectly indented prefix comments.
|
||||
@@ -89,7 +89,7 @@ async def test_async_unformatted( ): # Trailing comment with extra leading
|
||||
+ pass
|
||||
+
|
||||
|
||||
# Adding some unformated code covering a wide range of syntaxes.
|
||||
# Adding some unformatted code covering a wide range of syntaxes.
|
||||
|
||||
if True:
|
||||
- # Incorrectly indented prefix comments.
|
||||
@@ -207,7 +207,7 @@ def foo4(
|
||||
pass
|
||||
|
||||
|
||||
# Adding some unformated code covering a wide range of syntaxes.
|
||||
# Adding some unformatted code covering a wide range of syntaxes.
|
||||
|
||||
if True:
|
||||
# Incorrectly indented prefix comments.
|
||||
@@ -279,7 +279,7 @@ def foo3(
|
||||
|
||||
def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
|
||||
|
||||
# Adding some unformated code covering a wide range of syntaxes.
|
||||
# Adding some unformatted code covering a wide range of syntaxes.
|
||||
|
||||
if True:
|
||||
# Incorrectly indented prefix comments.
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/module_docstring_2.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
"""I am a very helpful module docstring.
|
||||
|
||||
With trailing spaces:
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
Ut enim ad minim veniam,
|
||||
quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat.
|
||||
Duis aute irure dolor in reprehenderit in voluptate
|
||||
velit esse cillum dolore eu fugiat nulla pariatur.
|
||||
Excepteur sint occaecat cupidatat non proident,
|
||||
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
a = 1
|
||||
|
||||
|
||||
"""Look at me I'm a docstring...
|
||||
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
........................................................NOT!
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
b = 2
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,6 +1,6 @@
|
||||
"""I am a very helpful module docstring.
|
||||
|
||||
-With trailing spaces:
|
||||
+With trailing spaces:
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
Ut enim ad minim veniam,
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
"""I am a very helpful module docstring.
|
||||
|
||||
With trailing spaces:
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
Ut enim ad minim veniam,
|
||||
quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat.
|
||||
Duis aute irure dolor in reprehenderit in voluptate
|
||||
velit esse cillum dolore eu fugiat nulla pariatur.
|
||||
Excepteur sint occaecat cupidatat non proident,
|
||||
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
"""
|
||||
|
||||
a = 1
|
||||
|
||||
|
||||
"""Look at me I'm a docstring...
|
||||
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
........................................................NOT!
|
||||
"""
|
||||
|
||||
|
||||
b = 2
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
"""I am a very helpful module docstring.
|
||||
|
||||
With trailing spaces:
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
Ut enim ad minim veniam,
|
||||
quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat.
|
||||
Duis aute irure dolor in reprehenderit in voluptate
|
||||
velit esse cillum dolore eu fugiat nulla pariatur.
|
||||
Excepteur sint occaecat cupidatat non proident,
|
||||
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
"""
|
||||
|
||||
a = 1
|
||||
|
||||
|
||||
"""Look at me I'm a docstring...
|
||||
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
............................................................
|
||||
........................................................NOT!
|
||||
"""
|
||||
|
||||
|
||||
b = 2
|
||||
```
|
||||
|
||||
|
||||
@@ -23,18 +23,6 @@ if sys.version_info > (3, 7):
|
||||
def function_definition(self): ...
|
||||
assignment = 1
|
||||
def f2(self) -> str: ...
|
||||
|
||||
|
||||
class TopLevel:
|
||||
class Nested1:
|
||||
foo: int
|
||||
def bar(self): ...
|
||||
field = 1
|
||||
|
||||
class Nested2:
|
||||
def bar(self): ...
|
||||
foo: int
|
||||
field = 1
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
@@ -52,7 +40,7 @@ class TopLevel:
|
||||
outer_attr_after_inner_stub: int
|
||||
|
||||
class Inner:
|
||||
@@ -9,27 +11,35 @@
|
||||
@@ -9,14 +11,19 @@
|
||||
|
||||
outer_attr: int
|
||||
|
||||
@@ -72,22 +60,6 @@ class TopLevel:
|
||||
assignment = 1
|
||||
|
||||
def f2(self) -> str: ...
|
||||
|
||||
+
|
||||
class TopLevel:
|
||||
class Nested1:
|
||||
foo: int
|
||||
+
|
||||
def bar(self): ...
|
||||
|
||||
field = 1
|
||||
|
||||
class Nested2:
|
||||
def bar(self): ...
|
||||
+
|
||||
foo: int
|
||||
|
||||
field = 1
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
@@ -122,22 +94,6 @@ if sys.version_info > (3, 7):
|
||||
assignment = 1
|
||||
|
||||
def f2(self) -> str: ...
|
||||
|
||||
|
||||
class TopLevel:
|
||||
class Nested1:
|
||||
foo: int
|
||||
|
||||
def bar(self): ...
|
||||
|
||||
field = 1
|
||||
|
||||
class Nested2:
|
||||
def bar(self): ...
|
||||
|
||||
foo: int
|
||||
|
||||
field = 1
|
||||
```
|
||||
|
||||
## Black Output
|
||||
@@ -165,19 +121,6 @@ if sys.version_info > (3, 7):
|
||||
assignment = 1
|
||||
|
||||
def f2(self) -> str: ...
|
||||
|
||||
class TopLevel:
|
||||
class Nested1:
|
||||
foo: int
|
||||
def bar(self): ...
|
||||
|
||||
field = 1
|
||||
|
||||
class Nested2:
|
||||
def bar(self): ...
|
||||
foo: int
|
||||
|
||||
field = 1
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ z: (Short
|
||||
z: (int) = 2.3
|
||||
z: ((int)) = foo()
|
||||
|
||||
# In case I go for not enforcing parantheses, this might get improved at the same time
|
||||
# In case I go for not enforcing parentheses, this might get improved at the same time
|
||||
x = (
|
||||
z
|
||||
== 9999999999999999999999999999999999999999
|
||||
@@ -165,7 +165,7 @@ z: Short | Short2 | Short3 | Short4 = 8
|
||||
z: int = 2.3
|
||||
z: int = foo()
|
||||
|
||||
# In case I go for not enforcing parantheses, this might get improved at the same time
|
||||
# In case I go for not enforcing parentheses, this might get improved at the same time
|
||||
x = (
|
||||
z
|
||||
== 9999999999999999999999999999999999999999
|
||||
@@ -269,7 +269,7 @@ z: Short | Short2 | Short3 | Short4 = 8
|
||||
z: int = 2.3
|
||||
z: int = foo()
|
||||
|
||||
# In case I go for not enforcing parantheses, this might get improved at the same time
|
||||
# In case I go for not enforcing parentheses, this might get improved at the same time
|
||||
x = (
|
||||
z
|
||||
== 9999999999999999999999999999999999999999
|
||||
|
||||
@@ -67,26 +67,6 @@ class Cls:
|
||||
def method(self):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
async def async_fn():
|
||||
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
@decorated
|
||||
async def async_fn():
|
||||
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
def top_level(
|
||||
a: int,
|
||||
b: str,
|
||||
) -> Whatever[Generic, Something]:
|
||||
|
||||
def nested(x: int) -> int:
|
||||
pass
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
@@ -102,7 +82,7 @@ def top_level(
|
||||
# This is also now fine
|
||||
a = 123
|
||||
|
||||
@@ -14,52 +13,41 @@
|
||||
@@ -14,49 +13,39 @@
|
||||
a = 123
|
||||
|
||||
if y:
|
||||
@@ -149,19 +129,9 @@ def top_level(
|
||||
|
||||
|
||||
class Cls:
|
||||
-
|
||||
def method(self):
|
||||
-
|
||||
pass
|
||||
|
||||
|
||||
@@ -76,6 +64,5 @@
|
||||
a: int,
|
||||
b: str,
|
||||
) -> Whatever[Generic, Something]:
|
||||
-
|
||||
def nested(x: int) -> int:
|
||||
pass
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
@@ -218,23 +188,6 @@ def quux():
|
||||
class Cls:
|
||||
def method(self):
|
||||
pass
|
||||
|
||||
|
||||
async def async_fn():
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
@decorated
|
||||
async def async_fn():
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
def top_level(
|
||||
a: int,
|
||||
b: str,
|
||||
) -> Whatever[Generic, Something]:
|
||||
def nested(x: int) -> int:
|
||||
pass
|
||||
```
|
||||
|
||||
## Black Output
|
||||
@@ -299,28 +252,9 @@ def quux():
|
||||
|
||||
|
||||
class Cls:
|
||||
|
||||
def method(self):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
async def async_fn():
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
@decorated
|
||||
async def async_fn():
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
def top_level(
|
||||
a: int,
|
||||
b: str,
|
||||
) -> Whatever[Generic, Something]:
|
||||
|
||||
def nested(x: int) -> int:
|
||||
pass
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ a = [
|
||||
pass
|
||||
|
||||
|
||||
@@ -68,25 +68,23 @@
|
||||
@@ -68,13 +68,12 @@
|
||||
def foo():
|
||||
pass
|
||||
|
||||
@@ -226,11 +226,7 @@ a = [
|
||||
pass
|
||||
|
||||
|
||||
class Baz:
|
||||
-
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@@ -85,7 +84,7 @@
|
||||
def something(self):
|
||||
pass
|
||||
|
||||
@@ -431,7 +427,6 @@ def bar(a=1, b: bool = False):
|
||||
|
||||
|
||||
class Baz:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ this_will_also_become_one_line = ( # comment
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,46 +1,69 @@
|
||||
@@ -1,95 +1,138 @@
|
||||
-"""cow
|
||||
+(
|
||||
+ """cow
|
||||
@@ -271,21 +271,41 @@ this_will_also_become_one_line = ( # comment
|
||||
+ ),
|
||||
)
|
||||
textwrap.dedent("""A one-line triple-quoted string.""")
|
||||
textwrap.dedent("""A two-line triple-quoted string
|
||||
@@ -54,18 +77,24 @@
|
||||
-textwrap.dedent("""A two-line triple-quoted string
|
||||
-since it goes to the next line.""")
|
||||
-textwrap.dedent("""A three-line triple-quoted string
|
||||
+textwrap.dedent(
|
||||
+ """A two-line triple-quoted string
|
||||
+since it goes to the next line."""
|
||||
+)
|
||||
+textwrap.dedent(
|
||||
+ """A three-line triple-quoted string
|
||||
that not only goes to the next line
|
||||
-but also goes one line beyond.""")
|
||||
-textwrap.dedent("""\
|
||||
+but also goes one line beyond."""
|
||||
+)
|
||||
+textwrap.dedent(
|
||||
+ """\
|
||||
A triple-quoted string
|
||||
actually leveraging the textwrap.dedent functionality
|
||||
that ends in a trailing newline,
|
||||
representing e.g. file contents.
|
||||
""")
|
||||
-""")
|
||||
-path.write_text(textwrap.dedent("""\
|
||||
+"""
|
||||
+)
|
||||
+path.write_text(
|
||||
+ textwrap.dedent("""\
|
||||
+ textwrap.dedent(
|
||||
+ """\
|
||||
A triple-quoted string
|
||||
actually leveraging the textwrap.dedent functionality
|
||||
that ends in a trailing newline,
|
||||
representing e.g. file contents.
|
||||
-"""))
|
||||
-path.write_text(textwrap.dedent("""\
|
||||
+""")
|
||||
+"""
|
||||
+ )
|
||||
+)
|
||||
+path.write_text(
|
||||
+ textwrap.dedent(
|
||||
@@ -299,9 +319,29 @@ this_will_also_become_one_line = ( # comment
|
||||
+ )
|
||||
+)
|
||||
# Another use case
|
||||
data = yaml.load("""\
|
||||
-data = yaml.load("""\
|
||||
+data = yaml.load(
|
||||
+ """\
|
||||
a: 1
|
||||
@@ -85,11 +114,13 @@
|
||||
b: 2
|
||||
-""")
|
||||
+"""
|
||||
+)
|
||||
data = yaml.load(
|
||||
"""\
|
||||
a: 1
|
||||
b: 2
|
||||
""",
|
||||
)
|
||||
-data = yaml.load("""\
|
||||
+data = yaml.load(
|
||||
+ """\
|
||||
a: 1
|
||||
b: 2
|
||||
-""")
|
||||
+"""
|
||||
+)
|
||||
|
||||
MULTILINE = """
|
||||
foo
|
||||
""".replace("\n", "")
|
||||
@@ -316,7 +356,7 @@ this_will_also_become_one_line = ( # comment
|
||||
parser.usage += """
|
||||
Custom extra help summary.
|
||||
|
||||
@@ -156,16 +187,24 @@
|
||||
@@ -156,16 +199,24 @@
|
||||
10 LOAD_CONST 0 (None)
|
||||
12 RETURN_VALUE
|
||||
""" % (_C.__init__.__code__.co_firstlineno + 1,)
|
||||
@@ -347,7 +387,36 @@ this_will_also_become_one_line = ( # comment
|
||||
[
|
||||
"""cow
|
||||
moos""",
|
||||
@@ -198,7 +237,7 @@
|
||||
@@ -177,28 +228,32 @@
|
||||
|
||||
|
||||
def dastardly_default_value(
|
||||
- cow: String = json.loads("""this
|
||||
+ cow: String = json.loads(
|
||||
+ """this
|
||||
is
|
||||
quite
|
||||
the
|
||||
dastadardly
|
||||
-value!"""),
|
||||
+value!"""
|
||||
+ ),
|
||||
**kwargs,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
-print(f"""
|
||||
+print(
|
||||
+ f"""
|
||||
This {animal}
|
||||
moos and barks
|
||||
{animal} say
|
||||
-""")
|
||||
+"""
|
||||
+)
|
||||
msg = f"""The arguments {bad_arguments} were passed in.
|
||||
Please use `--build-option` instead,
|
||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
||||
"""
|
||||
|
||||
@@ -356,7 +425,7 @@ this_will_also_become_one_line = ( # comment
|
||||
|
||||
this_will_stay_on_three_lines = (
|
||||
"a" # comment
|
||||
@@ -206,4 +245,6 @@
|
||||
@@ -206,4 +261,6 @@
|
||||
"c"
|
||||
)
|
||||
|
||||
@@ -437,24 +506,32 @@ call(
|
||||
),
|
||||
)
|
||||
textwrap.dedent("""A one-line triple-quoted string.""")
|
||||
textwrap.dedent("""A two-line triple-quoted string
|
||||
since it goes to the next line.""")
|
||||
textwrap.dedent("""A three-line triple-quoted string
|
||||
textwrap.dedent(
|
||||
"""A two-line triple-quoted string
|
||||
since it goes to the next line."""
|
||||
)
|
||||
textwrap.dedent(
|
||||
"""A three-line triple-quoted string
|
||||
that not only goes to the next line
|
||||
but also goes one line beyond.""")
|
||||
textwrap.dedent("""\
|
||||
but also goes one line beyond."""
|
||||
)
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
A triple-quoted string
|
||||
actually leveraging the textwrap.dedent functionality
|
||||
that ends in a trailing newline,
|
||||
representing e.g. file contents.
|
||||
""")
|
||||
"""
|
||||
)
|
||||
path.write_text(
|
||||
textwrap.dedent("""\
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
A triple-quoted string
|
||||
actually leveraging the textwrap.dedent functionality
|
||||
that ends in a trailing newline,
|
||||
representing e.g. file contents.
|
||||
""")
|
||||
"""
|
||||
)
|
||||
)
|
||||
path.write_text(
|
||||
textwrap.dedent(
|
||||
@@ -467,20 +544,24 @@ path.write_text(
|
||||
)
|
||||
)
|
||||
# Another use case
|
||||
data = yaml.load("""\
|
||||
data = yaml.load(
|
||||
"""\
|
||||
a: 1
|
||||
b: 2
|
||||
""")
|
||||
"""
|
||||
)
|
||||
data = yaml.load(
|
||||
"""\
|
||||
a: 1
|
||||
b: 2
|
||||
""",
|
||||
)
|
||||
data = yaml.load("""\
|
||||
data = yaml.load(
|
||||
"""\
|
||||
a: 1
|
||||
b: 2
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
MULTILINE = """
|
||||
foo
|
||||
@@ -587,22 +668,26 @@ barks""",
|
||||
|
||||
|
||||
def dastardly_default_value(
|
||||
cow: String = json.loads("""this
|
||||
cow: String = json.loads(
|
||||
"""this
|
||||
is
|
||||
quite
|
||||
the
|
||||
dastadardly
|
||||
value!"""),
|
||||
value!"""
|
||||
),
|
||||
**kwargs,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
print(f"""
|
||||
print(
|
||||
f"""
|
||||
This {animal}
|
||||
moos and barks
|
||||
{animal} say
|
||||
""")
|
||||
"""
|
||||
)
|
||||
msg = f"""The arguments {bad_arguments} were passed in.
|
||||
Please use `--build-option` instead,
|
||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_pep_572.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
x[(a:=0):]
|
||||
x[:(a:=0)]
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,2 +1,2 @@
|
||||
-x[(a := 0):]
|
||||
-x[:(a := 0)]
|
||||
+x[(a := 0) :]
|
||||
+x[: (a := 0)]
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
x[(a := 0) :]
|
||||
x[: (a := 0)]
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
x[(a := 0):]
|
||||
x[:(a := 0)]
|
||||
```
|
||||
|
||||
|
||||
@@ -8209,42 +8209,6 @@ def markdown_skipped_rst_directive():
|
||||
```
|
||||
|
||||
|
||||
#### Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -480,10 +480,8 @@
|
||||
Do cool stuff::
|
||||
|
||||
if True:
|
||||
- cool_stuff(
|
||||
- '''
|
||||
- hiya'''
|
||||
- )
|
||||
+ cool_stuff('''
|
||||
+ hiya''')
|
||||
|
||||
Done.
|
||||
"""
|
||||
@@ -958,13 +956,11 @@
|
||||
Do cool stuff.
|
||||
|
||||
``````
|
||||
- do_something(
|
||||
- '''
|
||||
+ do_something('''
|
||||
```
|
||||
did i trick you?
|
||||
```
|
||||
- '''
|
||||
- )
|
||||
+ ''')
|
||||
``````
|
||||
|
||||
Done.
|
||||
```
|
||||
|
||||
|
||||
### Output 6
|
||||
```
|
||||
indent-style = space
|
||||
@@ -9613,42 +9577,6 @@ def markdown_skipped_rst_directive():
|
||||
```
|
||||
|
||||
|
||||
#### Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -480,10 +480,8 @@
|
||||
Do cool stuff::
|
||||
|
||||
if True:
|
||||
- cool_stuff(
|
||||
- '''
|
||||
- hiya'''
|
||||
- )
|
||||
+ cool_stuff('''
|
||||
+ hiya''')
|
||||
|
||||
Done.
|
||||
"""
|
||||
@@ -958,13 +956,11 @@
|
||||
Do cool stuff.
|
||||
|
||||
``````
|
||||
- do_something(
|
||||
- '''
|
||||
+ do_something('''
|
||||
```
|
||||
did i trick you?
|
||||
```
|
||||
- '''
|
||||
- )
|
||||
+ ''')
|
||||
``````
|
||||
|
||||
Done.
|
||||
```
|
||||
|
||||
|
||||
### Output 7
|
||||
```
|
||||
indent-style = tab
|
||||
@@ -11026,42 +10954,6 @@ def markdown_skipped_rst_directive():
|
||||
```
|
||||
|
||||
|
||||
#### Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -489,10 +489,8 @@
|
||||
Do cool stuff::
|
||||
|
||||
if True:
|
||||
- cool_stuff(
|
||||
- '''
|
||||
- hiya'''
|
||||
- )
|
||||
+ cool_stuff('''
|
||||
+ hiya''')
|
||||
|
||||
Done.
|
||||
"""
|
||||
@@ -967,13 +965,11 @@
|
||||
Do cool stuff.
|
||||
|
||||
``````
|
||||
- do_something(
|
||||
- '''
|
||||
+ do_something('''
|
||||
```
|
||||
did i trick you?
|
||||
```
|
||||
- '''
|
||||
- )
|
||||
+ ''')
|
||||
``````
|
||||
|
||||
Done.
|
||||
```
|
||||
|
||||
|
||||
### Output 8
|
||||
```
|
||||
indent-style = tab
|
||||
@@ -12430,42 +12322,6 @@ def markdown_skipped_rst_directive():
|
||||
```
|
||||
|
||||
|
||||
#### Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -480,10 +480,8 @@
|
||||
Do cool stuff::
|
||||
|
||||
if True:
|
||||
- cool_stuff(
|
||||
- '''
|
||||
- hiya'''
|
||||
- )
|
||||
+ cool_stuff('''
|
||||
+ hiya''')
|
||||
|
||||
Done.
|
||||
"""
|
||||
@@ -958,13 +956,11 @@
|
||||
Do cool stuff.
|
||||
|
||||
``````
|
||||
- do_something(
|
||||
- '''
|
||||
+ do_something('''
|
||||
```
|
||||
did i trick you?
|
||||
```
|
||||
- '''
|
||||
- )
|
||||
+ ''')
|
||||
``````
|
||||
|
||||
Done.
|
||||
```
|
||||
|
||||
|
||||
### Output 9
|
||||
```
|
||||
indent-style = space
|
||||
@@ -13843,42 +13699,6 @@ def markdown_skipped_rst_directive():
|
||||
```
|
||||
|
||||
|
||||
#### Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -489,10 +489,8 @@
|
||||
Do cool stuff::
|
||||
|
||||
if True:
|
||||
- cool_stuff(
|
||||
- '''
|
||||
- hiya'''
|
||||
- )
|
||||
+ cool_stuff('''
|
||||
+ hiya''')
|
||||
|
||||
Done.
|
||||
"""
|
||||
@@ -967,13 +965,11 @@
|
||||
Do cool stuff.
|
||||
|
||||
``````
|
||||
- do_something(
|
||||
- '''
|
||||
+ do_something('''
|
||||
```
|
||||
did i trick you?
|
||||
```
|
||||
- '''
|
||||
- )
|
||||
+ ''')
|
||||
``````
|
||||
|
||||
Done.
|
||||
```
|
||||
|
||||
|
||||
### Output 10
|
||||
```
|
||||
indent-style = space
|
||||
@@ -15247,40 +15067,4 @@ def markdown_skipped_rst_directive():
|
||||
```
|
||||
|
||||
|
||||
#### Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -480,10 +480,8 @@
|
||||
Do cool stuff::
|
||||
|
||||
if True:
|
||||
- cool_stuff(
|
||||
- '''
|
||||
- hiya'''
|
||||
- )
|
||||
+ cool_stuff('''
|
||||
+ hiya''')
|
||||
|
||||
Done.
|
||||
"""
|
||||
@@ -958,13 +956,11 @@
|
||||
Do cool stuff.
|
||||
|
||||
``````
|
||||
- do_something(
|
||||
- '''
|
||||
+ do_something('''
|
||||
```
|
||||
did i trick you?
|
||||
```
|
||||
- '''
|
||||
- )
|
||||
+ ''')
|
||||
``````
|
||||
|
||||
Done.
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/multiline_string_deviations.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
# This file documents the deviations for formatting multiline strings with black.
|
||||
|
||||
# Black hugs the parentheses for `%` usages -> convert to fstring.
|
||||
# Can get unreadable if the arguments split
|
||||
# This could be solved by using `best_fitting` to try to format the arguments on a single
|
||||
# line. Let's consider adding this later.
|
||||
# ```python
|
||||
# call(
|
||||
# 3,
|
||||
# "dogsay",
|
||||
# textwrap.dedent(
|
||||
# """dove
|
||||
# coo""" % "cowabunga",
|
||||
# more,
|
||||
# and_more,
|
||||
# "aaaaaaa",
|
||||
# "bbbbbbbbb",
|
||||
# "cccccccc",
|
||||
# ),
|
||||
# )
|
||||
# ```
|
||||
call(3, "dogsay", textwrap.dedent("""dove
|
||||
coo""" % "cowabunga"))
|
||||
|
||||
# Black applies the hugging recursively. We don't (consistent with the hugging style).
|
||||
path.write_text(textwrap.dedent("""\
|
||||
A triple-quoted string
|
||||
actually leveraging the textwrap.dedent functionality
|
||||
that ends in a trailing newline,
|
||||
representing e.g. file contents.
|
||||
"""))
|
||||
|
||||
|
||||
|
||||
# Black avoids parenthesizing the following lambda. We could potentially support
|
||||
# this by changing `Lambda::needs_parentheses` to return `BestFit` but it causes
|
||||
# issues when the lambda has comments.
|
||||
# Let's keep this as a known deviation for now.
|
||||
generated_readme = lambda project_name: """
|
||||
{}
|
||||
|
||||
<Add content here!>
|
||||
""".strip().format(project_name)
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
# This file documents the deviations for formatting multiline strings with black.
|
||||
|
||||
# Black hugs the parentheses for `%` usages -> convert to fstring.
|
||||
# Can get unreadable if the arguments split
|
||||
# This could be solved by using `best_fitting` to try to format the arguments on a single
|
||||
# line. Let's consider adding this later.
|
||||
# ```python
|
||||
# call(
|
||||
# 3,
|
||||
# "dogsay",
|
||||
# textwrap.dedent(
|
||||
# """dove
|
||||
# coo""" % "cowabunga",
|
||||
# more,
|
||||
# and_more,
|
||||
# "aaaaaaa",
|
||||
# "bbbbbbbbb",
|
||||
# "cccccccc",
|
||||
# ),
|
||||
# )
|
||||
# ```
|
||||
call(
|
||||
3,
|
||||
"dogsay",
|
||||
textwrap.dedent(
|
||||
"""dove
|
||||
coo"""
|
||||
% "cowabunga"
|
||||
),
|
||||
)
|
||||
|
||||
# Black applies the hugging recursively. We don't (consistent with the hugging style).
|
||||
path.write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
A triple-quoted string
|
||||
actually leveraging the textwrap.dedent functionality
|
||||
that ends in a trailing newline,
|
||||
representing e.g. file contents.
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Black avoids parenthesizing the following lambda. We could potentially support
|
||||
# this by changing `Lambda::needs_parentheses` to return `BestFit` but it causes
|
||||
# issues when the lambda has comments.
|
||||
# Let's keep this as a known deviation for now.
|
||||
generated_readme = (
|
||||
lambda project_name: """
|
||||
{}
|
||||
|
||||
<Add content here!>
|
||||
""".strip().format(project_name)
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -31,14 +31,12 @@
|
||||
|
||||
# Black applies the hugging recursively. We don't (consistent with the hugging style).
|
||||
path.write_text(
|
||||
- textwrap.dedent(
|
||||
- """\
|
||||
+ textwrap.dedent("""\
|
||||
A triple-quoted string
|
||||
actually leveraging the textwrap.dedent functionality
|
||||
that ends in a trailing newline,
|
||||
representing e.g. file contents.
|
||||
-"""
|
||||
- )
|
||||
+""")
|
||||
)
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_parser::lexer::{lex, LexResult, LexicalError};
|
||||
use ruff_python_parser::lexer::{lex, LexicalError};
|
||||
use ruff_python_parser::{AsMode, Tok};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_text_size::TextRange;
|
||||
@@ -27,16 +27,15 @@ impl CommentRangesBuilder {
|
||||
pub fn tokens_and_ranges(
|
||||
source: &str,
|
||||
source_type: PySourceType,
|
||||
) -> Result<(Vec<LexResult>, CommentRanges), LexicalError> {
|
||||
) -> Result<(Vec<(Tok, TextRange)>, CommentRanges), LexicalError> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut comment_ranges = CommentRangesBuilder::default();
|
||||
|
||||
for result in lex(source, source_type.as_mode()) {
|
||||
if let Ok((token, range)) = &result {
|
||||
comment_ranges.visit_token(token, *range);
|
||||
}
|
||||
let (token, range) = result?;
|
||||
|
||||
tokens.push(result);
|
||||
comment_ranges.visit_token(&token, range);
|
||||
tokens.push((token, range));
|
||||
}
|
||||
|
||||
let comment_ranges = comment_ranges.finish();
|
||||
|
||||
@@ -687,7 +687,7 @@ mod tests {
|
||||
|
||||
let src = r"!foo = 42";
|
||||
let tokens = crate::lexer::lex(src, Mode::Ipython);
|
||||
let ast = crate::parse_tokens(tokens.collect(), src, Mode::Ipython);
|
||||
let ast = crate::parse_tokens(tokens, src, Mode::Ipython);
|
||||
insta::assert_debug_snapshot!(ast);
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
//! return bool(i & 1)
|
||||
//! "#;
|
||||
//! let tokens = lex(python_source, Mode::Module);
|
||||
//! let ast = parse_tokens(tokens.collect(), python_source, Mode::Module);
|
||||
//! let ast = parse_tokens(tokens, python_source, Mode::Module);
|
||||
//!
|
||||
//! assert!(ast.is_ok());
|
||||
//! ```
|
||||
@@ -110,8 +110,8 @@
|
||||
//! [lexer]: crate::lexer
|
||||
|
||||
pub use parser::{
|
||||
parse, parse_expression, parse_expression_starts_at, parse_program, parse_starts_at,
|
||||
parse_suite, parse_tokens, ParseError, ParseErrorType,
|
||||
parse, parse_expression, parse_expression_starts_at, parse_ok_tokens, parse_program,
|
||||
parse_starts_at, parse_suite, parse_tokens, ParseError, ParseErrorType,
|
||||
};
|
||||
use ruff_python_ast::{Mod, PySourceType, Suite};
|
||||
pub use string::FStringErrorType;
|
||||
@@ -128,7 +128,6 @@ mod parser;
|
||||
mod soft_keywords;
|
||||
mod string;
|
||||
mod token;
|
||||
mod token_source;
|
||||
pub mod typing;
|
||||
|
||||
/// Collect tokens up to and including the first error.
|
||||
@@ -146,7 +145,7 @@ pub fn tokenize(contents: &str, mode: Mode) -> Vec<LexResult> {
|
||||
|
||||
/// Parse a full Python program from its tokens.
|
||||
pub fn parse_program_tokens(
|
||||
tokens: Vec<LexResult>,
|
||||
lxr: Vec<LexResult>,
|
||||
source: &str,
|
||||
is_jupyter_notebook: bool,
|
||||
) -> anyhow::Result<Suite, ParseError> {
|
||||
@@ -155,7 +154,7 @@ pub fn parse_program_tokens(
|
||||
} else {
|
||||
Mode::Module
|
||||
};
|
||||
match parse_tokens(tokens, source, mode)? {
|
||||
match parse_tokens(lxr, source, mode)? {
|
||||
Mod::Module(m) => Ok(m.body),
|
||||
Mod::Expression(_) => unreachable!("Mode::Module doesn't return other variant"),
|
||||
}
|
||||
|
||||
@@ -14,7 +14,15 @@
|
||||
|
||||
use itertools::Itertools;
|
||||
pub(super) use lalrpop_util::ParseError as LalrpopError;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::lexer::{lex, lex_starts_at, Spanned};
|
||||
use crate::{
|
||||
lexer::{self, LexResult, LexicalError, LexicalErrorType},
|
||||
python,
|
||||
token::Tok,
|
||||
Mode,
|
||||
};
|
||||
use ruff_python_ast::{
|
||||
Expr, ExprAttribute, ExprAwait, ExprBinOp, ExprBoolOp, ExprBooleanLiteral, ExprBytesLiteral,
|
||||
ExprCall, ExprCompare, ExprDict, ExprDictComp, ExprEllipsisLiteral, ExprFString,
|
||||
@@ -23,16 +31,6 @@ use ruff_python_ast::{
|
||||
ExprStarred, ExprStringLiteral, ExprSubscript, ExprTuple, ExprUnaryOp, ExprYield,
|
||||
ExprYieldFrom, Mod, ModModule, Suite,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::lexer::{lex, lex_starts_at, LexResult};
|
||||
use crate::token_source::TokenSource;
|
||||
use crate::{
|
||||
lexer::{self, LexicalError, LexicalErrorType},
|
||||
python,
|
||||
token::Tok,
|
||||
Mode,
|
||||
};
|
||||
|
||||
/// Parse a full Python program usually consisting of multiple lines.
|
||||
///
|
||||
@@ -56,7 +54,7 @@ use crate::{
|
||||
/// ```
|
||||
pub fn parse_program(source: &str) -> Result<ModModule, ParseError> {
|
||||
let lexer = lex(source, Mode::Module);
|
||||
match parse_tokens(lexer.collect(), source, Mode::Module)? {
|
||||
match parse_tokens(lexer, source, Mode::Module)? {
|
||||
Mod::Module(m) => Ok(m),
|
||||
Mod::Expression(_) => unreachable!("Mode::Module doesn't return other variant"),
|
||||
}
|
||||
@@ -84,7 +82,7 @@ pub fn parse_suite(source: &str) -> Result<Suite, ParseError> {
|
||||
/// ```
|
||||
pub fn parse_expression(source: &str) -> Result<Expr, ParseError> {
|
||||
let lexer = lex(source, Mode::Expression);
|
||||
match parse_tokens(lexer.collect(), source, Mode::Expression)? {
|
||||
match parse_tokens(lexer, source, Mode::Expression)? {
|
||||
Mod::Expression(expression) => Ok(*expression.body),
|
||||
Mod::Module(_m) => unreachable!("Mode::Expression doesn't return other variant"),
|
||||
}
|
||||
@@ -109,7 +107,7 @@ pub fn parse_expression(source: &str) -> Result<Expr, ParseError> {
|
||||
/// ```
|
||||
pub fn parse_expression_starts_at(source: &str, offset: TextSize) -> Result<Expr, ParseError> {
|
||||
let lexer = lex_starts_at(source, Mode::Module, offset);
|
||||
match parse_tokens(lexer.collect(), source, Mode::Expression)? {
|
||||
match parse_tokens(lexer, source, Mode::Expression)? {
|
||||
Mod::Expression(expression) => Ok(*expression.body),
|
||||
Mod::Module(_m) => unreachable!("Mode::Expression doesn't return other variant"),
|
||||
}
|
||||
@@ -190,7 +188,7 @@ pub fn parse(source: &str, mode: Mode) -> Result<Mod, ParseError> {
|
||||
/// ```
|
||||
pub fn parse_starts_at(source: &str, mode: Mode, offset: TextSize) -> Result<Mod, ParseError> {
|
||||
let lxr = lexer::lex_starts_at(source, mode, offset);
|
||||
parse_tokens(lxr.collect(), source, mode)
|
||||
parse_tokens(lxr, source, mode)
|
||||
}
|
||||
|
||||
/// Parse an iterator of [`LexResult`]s using the specified [`Mode`].
|
||||
@@ -206,12 +204,48 @@ pub fn parse_starts_at(source: &str, mode: Mode, offset: TextSize) -> Result<Mod
|
||||
/// use ruff_python_parser::{lexer::lex, Mode, parse_tokens};
|
||||
///
|
||||
/// let source = "1 + 2";
|
||||
/// let expr = parse_tokens(lex(source, Mode::Expression).collect(), source, Mode::Expression);
|
||||
/// let expr = parse_tokens(lex(source, Mode::Expression), source, Mode::Expression);
|
||||
/// assert!(expr.is_ok());
|
||||
/// ```
|
||||
pub fn parse_tokens(tokens: Vec<LexResult>, source: &str, mode: Mode) -> Result<Mod, ParseError> {
|
||||
pub fn parse_tokens(
|
||||
lxr: impl IntoIterator<Item = LexResult>,
|
||||
source: &str,
|
||||
mode: Mode,
|
||||
) -> Result<Mod, ParseError> {
|
||||
let lxr = lxr.into_iter();
|
||||
|
||||
parse_filtered_tokens(
|
||||
lxr.filter_ok(|(tok, _)| !matches!(tok, Tok::Comment { .. } | Tok::NonLogicalNewline)),
|
||||
source,
|
||||
mode,
|
||||
)
|
||||
}
|
||||
|
||||
/// Parse tokens into an AST like [`parse_tokens`], but we already know all tokens are valid.
|
||||
pub fn parse_ok_tokens(
|
||||
lxr: impl IntoIterator<Item = Spanned>,
|
||||
source: &str,
|
||||
mode: Mode,
|
||||
) -> Result<Mod, ParseError> {
|
||||
let lxr = lxr
|
||||
.into_iter()
|
||||
.filter(|(tok, _)| !matches!(tok, Tok::Comment { .. } | Tok::NonLogicalNewline));
|
||||
let marker_token = (Tok::start_marker(mode), TextRange::default());
|
||||
let lexer = std::iter::once(Ok(marker_token)).chain(TokenSource::new(tokens));
|
||||
let lexer = std::iter::once(marker_token)
|
||||
.chain(lxr)
|
||||
.map(|(t, range)| (range.start(), t, range.end()));
|
||||
python::TopParser::new()
|
||||
.parse(source, mode, lexer)
|
||||
.map_err(parse_error_from_lalrpop)
|
||||
}
|
||||
|
||||
fn parse_filtered_tokens(
|
||||
lxr: impl IntoIterator<Item = LexResult>,
|
||||
source: &str,
|
||||
mode: Mode,
|
||||
) -> Result<Mod, ParseError> {
|
||||
let marker_token = (Tok::start_marker(mode), TextRange::default());
|
||||
let lexer = std::iter::once(Ok(marker_token)).chain(lxr);
|
||||
python::TopParser::new()
|
||||
.parse(
|
||||
source,
|
||||
@@ -563,9 +597,8 @@ impl From<ExprSlice> for ParenthesizedExpr {
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
mod size_assertions {
|
||||
use static_assertions::assert_eq_size;
|
||||
|
||||
use crate::parser::ParenthesizedExpr;
|
||||
use static_assertions::assert_eq_size;
|
||||
|
||||
assert_eq_size!(ParenthesizedExpr, [u8; 88]);
|
||||
}
|
||||
@@ -1442,7 +1475,7 @@ a = 1
|
||||
"
|
||||
.trim();
|
||||
let lxr = lexer::lex_starts_at(source, Mode::Ipython, TextSize::default());
|
||||
let parse_err = parse_tokens(lxr.collect(), source, Mode::Module).unwrap_err();
|
||||
let parse_err = parse_tokens(lxr, source, Mode::Module).unwrap_err();
|
||||
assert_eq!(
|
||||
parse_err.to_string(),
|
||||
"IPython escape commands are only allowed in `Mode::Ipython` at byte offset 6"
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
use crate::lexer::LexResult;
|
||||
use crate::Tok;
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct TokenSource {
|
||||
tokens: std::vec::IntoIter<LexResult>,
|
||||
}
|
||||
|
||||
impl TokenSource {
|
||||
pub(crate) fn new(tokens: Vec<LexResult>) -> Self {
|
||||
Self {
|
||||
tokens: tokens.into_iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<LexResult> for TokenSource {
|
||||
#[inline]
|
||||
fn from_iter<T: IntoIterator<Item = LexResult>>(iter: T) -> Self {
|
||||
Self::new(Vec::from_iter(iter))
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for TokenSource {
|
||||
type Item = LexResult;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
let next = self.tokens.next()?;
|
||||
|
||||
if is_trivia(&next) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break Some(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for TokenSource {}
|
||||
|
||||
const fn is_trivia(result: &LexResult) -> bool {
|
||||
matches!(result, Ok((Tok::Comment(_) | Tok::NonLogicalNewline, _)))
|
||||
}
|
||||
@@ -984,11 +984,6 @@ impl<'a> SemanticModel<'a> {
|
||||
scope.parent.map(|scope_id| &self.scopes[scope_id])
|
||||
}
|
||||
|
||||
/// Returns the ID of the parent of the given [`ScopeId`], if any.
|
||||
pub fn parent_scope_id(&self, scope_id: ScopeId) -> Option<ScopeId> {
|
||||
self.scopes[scope_id].parent
|
||||
}
|
||||
|
||||
/// Returns the first parent of the given [`Scope`] that is not of [`ScopeKind::Type`], if any.
|
||||
pub fn first_non_type_parent_scope(&self, scope: &Scope) -> Option<&Scope<'a>> {
|
||||
let mut current_scope = scope;
|
||||
@@ -1002,19 +997,6 @@ impl<'a> SemanticModel<'a> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the first parent of the given [`ScopeId`] that is not of [`ScopeKind::Type`], if any.
|
||||
pub fn first_non_type_parent_scope_id(&self, scope_id: ScopeId) -> Option<ScopeId> {
|
||||
let mut current_scope_id = scope_id;
|
||||
while let Some(parent_id) = self.parent_scope_id(current_scope_id) {
|
||||
if self.scopes[parent_id].kind.is_type() {
|
||||
current_scope_id = parent_id;
|
||||
} else {
|
||||
return Some(parent_id);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Return the [`Stmt`] corresponding to the given [`NodeId`].
|
||||
#[inline]
|
||||
pub fn node(&self, node_id: NodeId) -> &NodeRef<'a> {
|
||||
|
||||
@@ -216,7 +216,7 @@ mod tests {
|
||||
fn block_comments_two_line_block_at_start() {
|
||||
// arrange
|
||||
let source = "# line 1\n# line 2\n";
|
||||
let tokens = tokenize(source, Mode::Module);
|
||||
let tokens: Vec<LexResult> = tokenize(source, Mode::Module);
|
||||
let locator = Locator::new(source);
|
||||
let indexer = Indexer::from_tokens(&tokens, &locator);
|
||||
|
||||
@@ -231,7 +231,7 @@ mod tests {
|
||||
fn block_comments_indented_block() {
|
||||
// arrange
|
||||
let source = " # line 1\n # line 2\n";
|
||||
let tokens = tokenize(source, Mode::Module);
|
||||
let tokens: Vec<LexResult> = tokenize(source, Mode::Module);
|
||||
let locator = Locator::new(source);
|
||||
let indexer = Indexer::from_tokens(&tokens, &locator);
|
||||
|
||||
@@ -261,7 +261,7 @@ mod tests {
|
||||
fn block_comments_lines_with_code_not_a_block() {
|
||||
// arrange
|
||||
let source = "x = 1 # line 1\ny = 2 # line 2\n";
|
||||
let tokens = tokenize(source, Mode::Module);
|
||||
let tokens: Vec<LexResult> = tokenize(source, Mode::Module);
|
||||
let locator = Locator::new(source);
|
||||
let indexer = Indexer::from_tokens(&tokens, &locator);
|
||||
|
||||
@@ -276,7 +276,7 @@ mod tests {
|
||||
fn block_comments_sequential_lines_not_in_block() {
|
||||
// arrange
|
||||
let source = " # line 1\n # line 2\n";
|
||||
let tokens = tokenize(source, Mode::Module);
|
||||
let tokens: Vec<LexResult> = tokenize(source, Mode::Module);
|
||||
let locator = Locator::new(source);
|
||||
let indexer = Indexer::from_tokens(&tokens, &locator);
|
||||
|
||||
@@ -296,7 +296,7 @@ mod tests {
|
||||
# line 2
|
||||
"""
|
||||
"#;
|
||||
let tokens = tokenize(source, Mode::Module);
|
||||
let tokens: Vec<LexResult> = tokenize(source, Mode::Module);
|
||||
let locator = Locator::new(source);
|
||||
let indexer = Indexer::from_tokens(&tokens, &locator);
|
||||
|
||||
@@ -333,7 +333,7 @@ y = 2 # do not form a block comment
|
||||
# therefore do not form a block comment
|
||||
"""
|
||||
"#;
|
||||
let tokens = tokenize(source, Mode::Module);
|
||||
let tokens: Vec<LexResult> = tokenize(source, Mode::Module);
|
||||
let locator = Locator::new(source);
|
||||
let indexer = Indexer::from_tokens(&tokens, &locator);
|
||||
|
||||
|
||||
@@ -24,14 +24,16 @@ ruff_macros = { path = "../ruff_macros" }
|
||||
anyhow = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
glob = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
ignore = { workspace = true }
|
||||
is-macro = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
log = { workspace = true }
|
||||
glob = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
matchit = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
path-absolutize = { workspace = true }
|
||||
path-slash = { workspace = true }
|
||||
pep440_rs = { workspace = true, features = ["serde"] }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
//! filesystem.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::RwLock;
|
||||
@@ -11,9 +10,11 @@ use anyhow::Result;
|
||||
use anyhow::{anyhow, bail};
|
||||
use globset::{Candidate, GlobSet};
|
||||
use ignore::{WalkBuilder, WalkState};
|
||||
use itertools::{Either, Itertools};
|
||||
use itertools::Itertools;
|
||||
use log::debug;
|
||||
use matchit::{InsertError, Router};
|
||||
use path_absolutize::path_dedot;
|
||||
use path_slash::PathExt;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use ruff_linter::fs;
|
||||
@@ -25,7 +26,6 @@ use crate::pyproject::settings_toml;
|
||||
use crate::settings::Settings;
|
||||
|
||||
/// The configuration information from a `pyproject.toml` file.
|
||||
#[derive(Debug)]
|
||||
pub struct PyprojectConfig {
|
||||
/// The strategy used to discover the relevant `pyproject.toml` file for
|
||||
/// each Python file.
|
||||
@@ -64,12 +64,10 @@ pub enum PyprojectDiscoveryStrategy {
|
||||
}
|
||||
|
||||
impl PyprojectDiscoveryStrategy {
|
||||
#[inline]
|
||||
pub const fn is_fixed(self) -> bool {
|
||||
matches!(self, PyprojectDiscoveryStrategy::Fixed)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn is_hierarchical(self) -> bool {
|
||||
matches!(self, PyprojectDiscoveryStrategy::Hierarchical)
|
||||
}
|
||||
@@ -86,79 +84,61 @@ pub enum Relativity {
|
||||
}
|
||||
|
||||
impl Relativity {
|
||||
pub fn resolve(self, path: &Path) -> PathBuf {
|
||||
pub fn resolve(self, path: &Path) -> &Path {
|
||||
match self {
|
||||
Relativity::Parent => path
|
||||
.parent()
|
||||
.expect("Expected pyproject.toml file to be in parent directory")
|
||||
.to_path_buf(),
|
||||
Relativity::Cwd => path_dedot::CWD.clone(),
|
||||
.expect("Expected `pyproject.toml` file to be in parent directory"),
|
||||
Relativity::Cwd => &path_dedot::CWD,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Resolver<'a> {
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
settings: BTreeMap<PathBuf, Settings>,
|
||||
#[derive(Default)]
|
||||
pub struct Resolver {
|
||||
/// All [`Settings`] that have been inserted into this [`Resolver`].
|
||||
settings: Vec<Settings>,
|
||||
/// A router from path to index in the [`Settings`] vector.
|
||||
router: Router<usize>,
|
||||
}
|
||||
|
||||
impl<'a> Resolver<'a> {
|
||||
/// Create a new [`Resolver`] for the given [`PyprojectConfig`].
|
||||
pub fn new(pyproject_config: &'a PyprojectConfig) -> Self {
|
||||
Self {
|
||||
pyproject_config,
|
||||
settings: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Settings`] from the [`PyprojectConfig`].
|
||||
#[inline]
|
||||
pub fn base_settings(&self) -> &Settings {
|
||||
&self.pyproject_config.settings
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Resolver`] is using a hierarchical discovery strategy.
|
||||
#[inline]
|
||||
pub fn is_hierarchical(&self) -> bool {
|
||||
self.pyproject_config.strategy.is_hierarchical()
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Resolver`] should force-exclude files passed directly to the CLI.
|
||||
#[inline]
|
||||
pub fn force_exclude(&self) -> bool {
|
||||
self.pyproject_config.settings.file_resolver.force_exclude
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Resolver`] should respect `.gitignore` files.
|
||||
#[inline]
|
||||
pub fn respect_gitignore(&self) -> bool {
|
||||
self.pyproject_config
|
||||
.settings
|
||||
.file_resolver
|
||||
.respect_gitignore
|
||||
}
|
||||
|
||||
impl Resolver {
|
||||
/// Add a resolved [`Settings`] under a given [`PathBuf`] scope.
|
||||
fn add(&mut self, path: PathBuf, settings: Settings) {
|
||||
self.settings.insert(path, settings);
|
||||
fn add(&mut self, path: &Path, settings: Settings) -> Result<()> {
|
||||
self.settings.push(settings);
|
||||
println!("path: {:?}", path.to_slash_lossy());
|
||||
match self.router.insert(
|
||||
format!("{}/*filepath", path.to_slash_lossy()),
|
||||
self.settings.len() - 1,
|
||||
) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(InsertError::Conflict { .. }) => Ok(()),
|
||||
Err(err) => Err(anyhow!("Failed to insert path into router: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the appropriate [`Settings`] for a given [`Path`].
|
||||
pub fn resolve(&self, path: &Path) -> &Settings {
|
||||
match self.pyproject_config.strategy {
|
||||
PyprojectDiscoveryStrategy::Fixed => &self.pyproject_config.settings,
|
||||
pub fn resolve<'a>(
|
||||
&'a self,
|
||||
path: &Path,
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
) -> &'a Settings {
|
||||
match pyproject_config.strategy {
|
||||
PyprojectDiscoveryStrategy::Fixed => &pyproject_config.settings,
|
||||
PyprojectDiscoveryStrategy::Hierarchical => self
|
||||
.settings
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|(root, settings)| path.starts_with(root).then_some(settings))
|
||||
.unwrap_or(&self.pyproject_config.settings),
|
||||
.router
|
||||
.at(&path.to_slash_lossy())
|
||||
.map(|match_| &self.settings[*match_.value])
|
||||
.unwrap_or(&pyproject_config.settings),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a mapping from Python package to its package root.
|
||||
pub fn package_roots(&'a self, files: &[&'a Path]) -> FxHashMap<&'a Path, Option<&'a Path>> {
|
||||
pub fn package_roots<'a>(
|
||||
&'a self,
|
||||
files: &[&'a Path],
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
) -> FxHashMap<&'a Path, Option<&'a Path>> {
|
||||
// Pre-populate the module cache, since the list of files could (but isn't
|
||||
// required to) contain some `__init__.py` files.
|
||||
let mut package_cache: FxHashMap<&Path, bool> = FxHashMap::default();
|
||||
@@ -170,32 +150,21 @@ impl<'a> Resolver<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether any of the settings require namespace packages. If not, we can save
|
||||
// a lookup for every file.
|
||||
let has_namespace_packages = self
|
||||
.settings
|
||||
.values()
|
||||
.any(|settings| !settings.linter.namespace_packages.is_empty());
|
||||
|
||||
// Search for the package root for each file.
|
||||
let mut package_roots: FxHashMap<&Path, Option<&Path>> = FxHashMap::default();
|
||||
for file in files {
|
||||
let namespace_packages = &self
|
||||
.resolve(file, pyproject_config)
|
||||
.linter
|
||||
.namespace_packages;
|
||||
if let Some(package) = file.parent() {
|
||||
match package_roots.entry(package) {
|
||||
std::collections::hash_map::Entry::Occupied(_) => continue,
|
||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||
let namespace_packages = if has_namespace_packages {
|
||||
self.resolve(file).linter.namespace_packages.as_slice()
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
entry.insert(detect_package_root_with_cache(
|
||||
package,
|
||||
namespace_packages,
|
||||
&mut package_cache,
|
||||
));
|
||||
}
|
||||
if package_roots.contains_key(package) {
|
||||
continue;
|
||||
}
|
||||
package_roots.insert(
|
||||
package,
|
||||
detect_package_root_with_cache(package, namespace_packages, &mut package_cache),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,12 +173,7 @@ impl<'a> Resolver<'a> {
|
||||
|
||||
/// Return an iterator over the resolved [`Settings`] in this [`Resolver`].
|
||||
pub fn settings(&self) -> impl Iterator<Item = &Settings> {
|
||||
match self.pyproject_config.strategy {
|
||||
PyprojectDiscoveryStrategy::Fixed => {
|
||||
Either::Left(std::iter::once(&self.pyproject_config.settings))
|
||||
}
|
||||
PyprojectDiscoveryStrategy::Hierarchical => Either::Right(self.settings.values()),
|
||||
}
|
||||
self.settings.iter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +234,7 @@ fn resolve_configuration(
|
||||
let options = pyproject::load_options(&path)?;
|
||||
|
||||
let project_root = relativity.resolve(&path);
|
||||
let configuration = Configuration::from_options(options, &project_root)?;
|
||||
let configuration = Configuration::from_options(options, project_root)?;
|
||||
|
||||
// If extending, continue to collect.
|
||||
next = configuration.extend.as_ref().map(|extend| {
|
||||
@@ -298,14 +262,14 @@ fn resolve_configuration(
|
||||
|
||||
/// Extract the project root (scope) and [`Settings`] from a given
|
||||
/// `pyproject.toml`.
|
||||
fn resolve_scoped_settings(
|
||||
pyproject: &Path,
|
||||
fn resolve_scoped_settings<'a>(
|
||||
pyproject: &'a Path,
|
||||
relativity: Relativity,
|
||||
transformer: &dyn ConfigurationTransformer,
|
||||
) -> Result<(PathBuf, Settings)> {
|
||||
) -> Result<(&'a Path, Settings)> {
|
||||
let configuration = resolve_configuration(pyproject, relativity, transformer)?;
|
||||
let project_root = relativity.resolve(pyproject);
|
||||
let settings = configuration.into_settings(&project_root)?;
|
||||
let settings = configuration.into_settings(project_root)?;
|
||||
Ok((project_root, settings))
|
||||
}
|
||||
|
||||
@@ -321,25 +285,25 @@ pub fn resolve_root_settings(
|
||||
}
|
||||
|
||||
/// Find all Python (`.py`, `.pyi` and `.ipynb` files) in a set of paths.
|
||||
pub fn python_files_in_path<'a>(
|
||||
pub fn python_files_in_path(
|
||||
paths: &[PathBuf],
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
pyproject_config: &PyprojectConfig,
|
||||
transformer: &dyn ConfigurationTransformer,
|
||||
) -> Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver<'a>)> {
|
||||
) -> Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver)> {
|
||||
// Normalize every path (e.g., convert from relative to absolute).
|
||||
let mut paths: Vec<PathBuf> = paths.iter().map(fs::normalize_path).unique().collect();
|
||||
|
||||
// Search for `pyproject.toml` files in all parent directories.
|
||||
let mut resolver = Resolver::new(pyproject_config);
|
||||
let mut resolver = Resolver::default();
|
||||
let mut seen = FxHashSet::default();
|
||||
if resolver.is_hierarchical() {
|
||||
if pyproject_config.strategy.is_hierarchical() {
|
||||
for path in &paths {
|
||||
for ancestor in path.ancestors() {
|
||||
if seen.insert(ancestor) {
|
||||
if let Some(pyproject) = settings_toml(ancestor)? {
|
||||
let (root, settings) =
|
||||
resolve_scoped_settings(&pyproject, Relativity::Parent, transformer)?;
|
||||
resolver.add(root, settings);
|
||||
resolver.add(root, settings)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -348,8 +312,8 @@ pub fn python_files_in_path<'a>(
|
||||
}
|
||||
|
||||
// Check if the paths themselves are excluded.
|
||||
if resolver.force_exclude() {
|
||||
paths.retain(|path| !is_file_excluded(path, &resolver));
|
||||
if pyproject_config.settings.file_resolver.force_exclude {
|
||||
paths.retain(|path| !is_file_excluded(path, &resolver, pyproject_config));
|
||||
if paths.is_empty() {
|
||||
return Ok((vec![], resolver));
|
||||
}
|
||||
@@ -363,12 +327,11 @@ pub fn python_files_in_path<'a>(
|
||||
for path in rest_paths {
|
||||
builder.add(path);
|
||||
}
|
||||
builder.standard_filters(resolver.respect_gitignore());
|
||||
builder.standard_filters(pyproject_config.settings.file_resolver.respect_gitignore);
|
||||
builder.hidden(false);
|
||||
let walker = builder.build_parallel();
|
||||
|
||||
// Run the `WalkParallel` to collect all Python files.
|
||||
let is_hierarchical = resolver.is_hierarchical();
|
||||
let error: std::sync::Mutex<Result<()>> = std::sync::Mutex::new(Ok(()));
|
||||
let resolver: RwLock<Resolver> = RwLock::new(resolver);
|
||||
let files: std::sync::Mutex<Vec<Result<ResolvedFile, ignore::Error>>> =
|
||||
@@ -380,7 +343,7 @@ pub fn python_files_in_path<'a>(
|
||||
if entry.depth() > 0 {
|
||||
let path = entry.path();
|
||||
let resolver = resolver.read().unwrap();
|
||||
let settings = resolver.resolve(path);
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
if let Some(file_name) = path.file_name() {
|
||||
let file_path = Candidate::new(path);
|
||||
let file_basename = Candidate::new(file_name);
|
||||
@@ -408,7 +371,7 @@ pub fn python_files_in_path<'a>(
|
||||
|
||||
// Search for the `pyproject.toml` file in this directory, before we visit any
|
||||
// of its contents.
|
||||
if is_hierarchical {
|
||||
if pyproject_config.strategy.is_hierarchical() {
|
||||
if let Ok(entry) = &result {
|
||||
if entry
|
||||
.file_type()
|
||||
@@ -421,7 +384,13 @@ pub fn python_files_in_path<'a>(
|
||||
transformer,
|
||||
) {
|
||||
Ok((root, settings)) => {
|
||||
resolver.write().unwrap().add(root, settings);
|
||||
match resolver.write().unwrap().add(root, settings) {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
*error.lock().unwrap() = Err(err);
|
||||
return WalkState::Quit;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
*error.lock().unwrap() = Err(err);
|
||||
@@ -450,7 +419,7 @@ pub fn python_files_in_path<'a>(
|
||||
// Otherwise, check if the file is included.
|
||||
let path = entry.path();
|
||||
let resolver = resolver.read().unwrap();
|
||||
let settings = resolver.resolve(path);
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
if settings.file_resolver.include.is_match(path) {
|
||||
debug!("Included path via `include`: {:?}", path);
|
||||
Some(ResolvedFile::Nested(entry.into_path()))
|
||||
@@ -528,33 +497,38 @@ impl Ord for ResolvedFile {
|
||||
/// Return `true` if the Python file at [`Path`] is _not_ excluded.
|
||||
pub fn python_file_at_path(
|
||||
path: &Path,
|
||||
resolver: &mut Resolver,
|
||||
pyproject_config: &PyprojectConfig,
|
||||
transformer: &dyn ConfigurationTransformer,
|
||||
) -> Result<bool> {
|
||||
// Normalize the path (e.g., convert from relative to absolute).
|
||||
let path = fs::normalize_path(path);
|
||||
|
||||
// Search for `pyproject.toml` files in all parent directories.
|
||||
if resolver.is_hierarchical() {
|
||||
let mut resolver = Resolver::default();
|
||||
if pyproject_config.strategy.is_hierarchical() {
|
||||
for ancestor in path.ancestors() {
|
||||
if let Some(pyproject) = settings_toml(ancestor)? {
|
||||
let (root, settings) =
|
||||
resolve_scoped_settings(&pyproject, Relativity::Parent, transformer)?;
|
||||
resolver.add(root, settings);
|
||||
resolver.add(root, settings)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check exclusions.
|
||||
Ok(!is_file_excluded(&path, resolver))
|
||||
Ok(!is_file_excluded(&path, &resolver, pyproject_config))
|
||||
}
|
||||
|
||||
/// Return `true` if the given top-level [`Path`] should be excluded.
|
||||
fn is_file_excluded(path: &Path, resolver: &Resolver) -> bool {
|
||||
fn is_file_excluded(
|
||||
path: &Path,
|
||||
resolver: &Resolver,
|
||||
pyproject_strategy: &PyprojectConfig,
|
||||
) -> bool {
|
||||
// TODO(charlie): Respect gitignore.
|
||||
for path in path.ancestors() {
|
||||
let settings = resolver.resolve(path);
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
if let Some(file_name) = path.file_name() {
|
||||
let file_path = Candidate::new(path);
|
||||
let file_basename = Candidate::new(file_name);
|
||||
@@ -647,6 +621,7 @@ mod tests {
|
||||
#[test]
|
||||
fn rooted_exclusion() -> Result<()> {
|
||||
let package_root = test_resource_path("package");
|
||||
let resolver = Resolver::default();
|
||||
let pyproject_config = PyprojectConfig::new(
|
||||
PyprojectDiscoveryStrategy::Hierarchical,
|
||||
resolve_root_settings(
|
||||
@@ -656,19 +631,20 @@ mod tests {
|
||||
)?,
|
||||
None,
|
||||
);
|
||||
let resolver = Resolver::new(&pyproject_config);
|
||||
// src/app.py should not be excluded even if it lives in a hierarchy that should
|
||||
// be excluded by virtue of the pyproject.toml having `resources/*` in
|
||||
// it.
|
||||
assert!(!is_file_excluded(
|
||||
&package_root.join("src/app.py"),
|
||||
&resolver,
|
||||
&pyproject_config,
|
||||
));
|
||||
// However, resources/ignored.py should be ignored, since that `resources` is
|
||||
// beneath the package root.
|
||||
assert!(is_file_excluded(
|
||||
&package_root.join("resources/ignored.py"),
|
||||
&resolver,
|
||||
&pyproject_config,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -138,7 +138,8 @@ For the full list of supported settings, see [_Settings_](settings.md#format). F
|
||||
configuring Ruff via `pyproject.toml`, see [_Configuring Ruff_](configuration.md).
|
||||
|
||||
Given the focus on Black compatibility (and unlike formatters like [YAPF](https://github.com/google/yapf)),
|
||||
Ruff does not currently expose any other configuration options.
|
||||
Ruff does not currently expose any configuration options to modify core formatting behavior outside
|
||||
of these trivia-related settings.
|
||||
|
||||
## Docstring formatting
|
||||
|
||||
|
||||
@@ -18,11 +18,9 @@ cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
ruff_linter = { path = "../crates/ruff_linter" }
|
||||
ruff_python_ast = { path = "../crates/ruff_python_ast" }
|
||||
ruff_python_codegen = { path = "../crates/ruff_python_codegen" }
|
||||
ruff_python_parser = { path = "../crates/ruff_python_parser" }
|
||||
ruff_source_file = { path = "../crates/ruff_source_file" }
|
||||
ruff_python_formatter = { path = "../crates/ruff_python_formatter"}
|
||||
|
||||
arbitrary = { version = "1.3.0", features = ["derive"] }
|
||||
libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false }
|
||||
@@ -40,18 +38,10 @@ path = "fuzz_targets/ruff_parse_simple.rs"
|
||||
name = "ruff_fix_validity"
|
||||
path = "fuzz_targets/ruff_fix_validity.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "ruff_formatter_validity"
|
||||
path = "fuzz_targets/ruff_formatter_validity.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "ruff_parse_idempotency"
|
||||
path = "fuzz_targets/ruff_parse_idempotency.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "ruff_formatter_idempotency"
|
||||
path = "fuzz_targets/ruff_formatter_idempotency.rs"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
debug = true
|
||||
|
||||
@@ -101,16 +101,3 @@ This fuzz harness checks that fixes applied by Ruff do not introduce new errors
|
||||
[`ruff_linter::test::test_snippet`](../crates/ruff_linter/src/test.rs) testing utility.
|
||||
It currently is only configured to use default settings, but may be extended in future versions to
|
||||
test non-default linter settings.
|
||||
|
||||
### `ruff_formatter_idempotency`
|
||||
|
||||
This fuzz harness ensures that the formatter is [idempotent](https://en.wikipedia.org/wiki/Idempotence)
|
||||
which detects possible unsteady states of Ruff's formatter.
|
||||
|
||||
### `ruff_formatter_validity`
|
||||
|
||||
This fuzz harness checks that Ruff's formatter does not introduce new linter errors/warnings by
|
||||
linting once, counting the number of each error type, then formatting, then linting again and
|
||||
ensuring that the number of each error type does not increase across formats. This has the
|
||||
beneficial side effect of discovering cases where the linter does not discover a lint error when
|
||||
it should have due to a formatting inconsistency.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ruff_fix_validity
|
||||
@@ -1 +0,0 @@
|
||||
ruff_fix_validity
|
||||
@@ -1,47 +0,0 @@
|
||||
//! Fuzzer harness which double formats the input and access the idempotency or unsteady state of the
|
||||
//! ruff's formatter.
|
||||
|
||||
#![no_main]
|
||||
|
||||
use libfuzzer_sys::{fuzz_target, Corpus};
|
||||
use similar::TextDiff;
|
||||
|
||||
use ruff_python_formatter::{format_module_source, PyFormatOptions};
|
||||
|
||||
fn do_fuzz(case: &[u8]) -> Corpus {
|
||||
// Throw away inputs which aren't utf-8
|
||||
let Ok(code) = std::str::from_utf8(case) else {
|
||||
return Corpus::Reject;
|
||||
};
|
||||
|
||||
let options = PyFormatOptions::default();
|
||||
// format the code once
|
||||
if let Ok(formatted) = format_module_source(code, options.clone()) {
|
||||
let formatted = formatted.as_code();
|
||||
|
||||
// reformat the code second time
|
||||
if let Ok(reformatted) = format_module_source(formatted, options.clone()) {
|
||||
let reformatted = reformatted.as_code();
|
||||
|
||||
if formatted != reformatted {
|
||||
let diff = TextDiff::from_lines(formatted, reformatted)
|
||||
.unified_diff()
|
||||
.header("Formatted Once", "Formatted Twice")
|
||||
.to_string();
|
||||
panic!(
|
||||
"\nReformatting the code a second time resulted in formatting changes.\nInput: {:?}\ndiff:\n{}",
|
||||
code, diff
|
||||
);
|
||||
}
|
||||
} else {
|
||||
panic!(
|
||||
"Unable to format the code second time:\nInput:{:?}\nformatted:\n{:?}",
|
||||
code, formatted
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Corpus::Keep
|
||||
}
|
||||
|
||||
fuzz_target!(|case: &[u8]| -> Corpus { do_fuzz(case) });
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user