Compare commits

..

5 Commits

Author SHA1 Message Date
Charlie Marsh
b54b1c1f05 Add Windows 2024-01-09 00:53:08 -05:00
Charlie Marsh
ad1b5c52d5 Use ref 2024-01-09 00:23:28 -05:00
Charlie Marsh
bd3fe93f41 Revert "Use radix_trie"
This reverts commit 3c7cbbcb92.
2024-01-09 00:17:19 -05:00
Charlie Marsh
3c7cbbcb92 Use radix_trie 2024-01-09 00:09:27 -05:00
Charlie Marsh
41f1f8a4ca Add matchit 2024-01-09 00:02:26 -05:00
102 changed files with 935 additions and 2420 deletions

14
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

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

View File

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

View File

@@ -25,9 +25,10 @@ use ruff_notebook::NotebookIndex;
use ruff_python_ast::imports::ImportMap;
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::resolver::Resolver;
use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy, Resolver};
use ruff_workspace::Settings;
use crate::cache;
use crate::diagnostics::Diagnostics;
/// [`Path`] that is relative to the package root in [`PackageCache`].
@@ -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)
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
use ruff_cli::args::{CliOverrides, FormatArguments, FormatCommand, LogLevelArgs};
use ruff_cli::args::{FormatCommand, LogLevelArgs};
use ruff_cli::resolve::resolve;
use ruff_formatter::{FormatError, LineWidth, PrintError};
use ruff_linter::logging::LogLevel;
@@ -38,24 +38,24 @@ use ruff_python_formatter::{
use ruff_python_parser::ParseError;
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, CliOverrides)> {
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
#[allow(clippy::type_complexity)]
fn ruff_check_paths(
dirs: &[PathBuf],
) -> anyhow::Result<(
Vec<Result<ResolvedFile, ignore::Error>>,
Resolver,
PyprojectConfig,
)> {
let args_matches = FormatCommand::command()
.no_binary_name(true)
.get_matches_from(dirs);
let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?;
let (cli, overrides) = arguments.partition();
Ok((cli, overrides))
}
/// Find the [`PyprojectConfig`] to use for formatting.
fn find_pyproject_config(
cli: &FormatArguments,
overrides: &CliOverrides,
) -> anyhow::Result<PyprojectConfig> {
let mut pyproject_config = resolve(
cli.isolated,
cli.config.as_deref(),
overrides,
&overrides,
cli.stdin_filename.as_deref(),
)?;
// We don't want to format pyproject.toml
@@ -64,18 +64,11 @@ fn find_pyproject_config(
FilePattern::Builtin("*.pyi"),
])
.unwrap();
Ok(pyproject_config)
}
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
#[allow(clippy::type_complexity)]
fn ruff_check_paths<'a>(
pyproject_config: &'a PyprojectConfig,
cli: &FormatArguments,
overrides: &CliOverrides,
) -> anyhow::Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver<'a>)> {
let (paths, resolver) = python_files_in_path(&cli.files, pyproject_config, overrides)?;
Ok((paths, resolver))
let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?;
if paths.is_empty() {
bail!("no python files in {:?}", dirs)
}
Ok((paths, resolver, pyproject_config))
}
/// Collects statistics over the formatted files to compute the Jaccard index or the similarity
@@ -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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
#############################################

View File

@@ -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);

View File

@@ -242,7 +242,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if !checker.semantic.current_scope().kind.is_class() {
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker, class_def, id, *range,
);
}
} else {
if checker.enabled(Rule::BuiltinVariableShadowing) {
flake8_builtins::rules::builtin_variable_shadowing(checker, id, *range);
}
@@ -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,

View File

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

View File

@@ -472,7 +472,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Simplify, "300") => (RuleGroup::Stable, rules::flake8_simplify::rules::YodaConditions),
(Flake8Simplify, "401") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet),
(Flake8Simplify, "910") => (RuleGroup::Stable, rules::flake8_simplify::rules::DictGetWithNoneDefault),
(Flake8Simplify, "911") => (RuleGroup::Preview, rules::flake8_simplify::rules::ZipDictKeysAndValues),
// flake8-copyright
#[allow(deprecated)]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);

View File

@@ -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

View File

@@ -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,

View File

@@ -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 | ):

View File

@@ -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,

View File

@@ -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);

View File

@@ -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);

View File

@@ -11,7 +11,6 @@ class ClassWithSpaceParentheses:
class ClassWithEmptyFunc(object):
def func_with_blank_parentheses():
return 5

View File

@@ -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 "",
)
)

View File

@@ -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 "",
))

View File

@@ -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.

View File

@@ -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.

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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):
...

View File

@@ -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): ...

View File

@@ -79,7 +79,6 @@ def bar(a=1, b: bool = False):
class Baz:
def __init__(self):
pass

View File

@@ -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."

View File

@@ -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."

View File

@@ -1,2 +1,2 @@
x[(a := 0) :]
x[: (a := 0)]
x[(a := 0):]
x[:(a := 0)]

View File

@@ -1 +0,0 @@
{"target_version": "py312"}

View File

@@ -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))

View File

@@ -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))

View File

@@ -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
]

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)),
]
)?;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)
})

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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,
}
}

View File

@@ -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>(

View File

@@ -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();

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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);

View File

@@ -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.
///

View File

@@ -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
```

View File

@@ -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 "",
))
```

View File

@@ -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.

View File

@@ -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
```

View File

@@ -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
```

View File

@@ -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

View File

@@ -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
```

View File

@@ -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

View File

@@ -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`.

View File

@@ -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)]
```

View File

@@ -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.
```

View File

@@ -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.
-"""
- )
+""")
)
```

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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"),
}

View File

@@ -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"

View File

@@ -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, _)))
}

View File

@@ -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> {

View File

@@ -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);

View File

@@ -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 }

View File

@@ -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(())
}

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -1 +0,0 @@
ruff_fix_validity

View File

@@ -1 +0,0 @@
ruff_fix_validity

View File

@@ -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