Compare commits

..

2 Commits

Author SHA1 Message Date
Micha Reiser
5d2513e5a9 POC of Black's string preview style formatting 2023-09-21 12:38:03 +02:00
Micha Reiser
272306bf5a Introduce StringContinuation data structure 2023-09-21 08:01:39 +02:00
76 changed files with 762 additions and 2265 deletions

6
.gitignore vendored
View File

@@ -208,9 +208,3 @@ cython_debug/
# VIM
.*.sw?
.sw?
# Custom re-inclusions for the resolver test cases
!crates/ruff_python_resolver/resources/test/airflow/venv/
!crates/ruff_python_resolver/resources/test/airflow/venv/lib
!crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so
!crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so

41
Cargo.lock generated
View File

@@ -810,7 +810,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.291"
version = "0.0.290"
dependencies = [
"anyhow",
"clap",
@@ -1035,9 +1035,9 @@ dependencies = [
[[package]]
name = "indicatif"
version = "0.17.7"
version = "0.17.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25"
checksum = "0b297dc40733f23a0e52728a58fa9489a5b7638a324932de16b41adc3ef80730"
dependencies = [
"console",
"instant",
@@ -1075,9 +1075,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.32.0"
version = "1.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e02c584f4595792d09509a94cdb92a3cef7592b1eb2d9877ee6f527062d0ea"
checksum = "a0770b0a3d4c70567f0d58331f3088b0e4c4f56c9b8d764efe654b4a5d46de3a"
dependencies = [
"console",
"globset",
@@ -1484,6 +1484,16 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "number_prefix"
version = "0.4.0"
@@ -1888,9 +1898,9 @@ dependencies = [
[[package]]
name = "rayon"
version = "1.8.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
dependencies = [
"either",
"rayon-core",
@@ -1898,12 +1908,14 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.12.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"num_cpus",
]
[[package]]
@@ -2051,7 +2063,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.291"
version = "0.0.290"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2187,7 +2199,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.0.291"
version = "0.0.290"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2336,7 +2348,6 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"rustc-hash",
"schemars",
"serde",
"serde_json",
"similar",
@@ -2524,9 +2535,7 @@ dependencies = [
"ruff_formatter",
"ruff_linter",
"ruff_macros",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_source_file",
"rustc-hash",
"schemars",
"serde",
@@ -2807,9 +2816,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "smallvec"
version = "1.11.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "spin"

View File

@@ -21,7 +21,7 @@ filetime = { version = "0.2.20" }
glob = { version = "0.3.1" }
globset = { version = "0.4.10" }
ignore = { version = "0.4.20" }
insta = { version = "1.32.0", feature = ["filters", "glob"] }
insta = { version = "1.31.0", feature = ["filters", "glob"] }
is-macro = { version = "0.3.0" }
itertools = { version = "0.11.0" }
log = { version = "0.4.17" }
@@ -39,7 +39,7 @@ serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.107" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.2.1", features = ["inline"] }
smallvec = { version = "1.11.1" }
smallvec = { version = "1.10.0" }
static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.2" }

View File

@@ -30,7 +30,7 @@ An extremely fast Python linter, written in Rust.
- 🤝 Python 3.11 compatibility
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [700 built-in rules](https://docs.astral.sh/ruff/rules/)
- 📏 Over [600 built-in rules](https://docs.astral.sh/ruff/rules/)
- ⚖️ [Near-parity](https://docs.astral.sh/ruff/faq/#how-does-ruff-compare-to-flake8) with the
built-in Flake8 rule set
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.291
rev: v0.0.290
hooks:
- id: ruff
```
@@ -233,7 +233,7 @@ linting command.
<!-- Begin section: Rules -->
**Ruff supports over 700 lint rules**, many of which are inspired by popular tools like Flake8,
**Ruff supports over 600 lint rules**, many of which are inspired by popular tools like Flake8,
isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implements every rule in
Rust as a first-party feature.

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.291"
version = "0.0.290"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.291"
version = "0.0.290"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -48,7 +48,7 @@ itoa = { version = "1.0.6" }
log = { workspace = true }
notify = { version = "6.1.1" }
path-absolutize = { workspace = true, features = ["once_cell_cache"] }
rayon = { version = "1.8.0" }
rayon = { version = "1.7.0" }
regex = { workspace = true }
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
rustc-hash = { workspace = true }

View File

@@ -0,0 +1,3 @@
a = 1
__all__ = list(["a", "b"])

View File

@@ -1,13 +1,12 @@
use anyhow::{anyhow, Result};
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
#[allow(clippy::print_stdout)]
pub(crate) fn config(key: Option<&str>) -> Result<()> {
match key {
None => print!("{}", Options::metadata()),
Some(key) => match Options::metadata().find(key) {
Some(key) => match Options::metadata().get(key) {
None => {
return Err(anyhow!("Unknown option: {key}"));
}

View File

@@ -15,9 +15,9 @@ use ruff_linter::fs;
use ruff_linter::logging::LogLevel;
use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{format_module, FormatModuleError};
use ruff_python_formatter::{format_module, FormatModuleError, PyFormatOptions};
use ruff_source_file::{find_newline, LineEnding};
use ruff_workspace::resolver::python_files_in_path;
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
use crate::panic::{catch_unwind, PanicError};
@@ -73,17 +73,15 @@ pub(crate) fn format(
};
let resolved_settings = resolver.resolve(path, &pyproject_config);
let options = resolved_settings.formatter.to_format_options(source_type);
debug!("Formatting {} with {:?}", path.display(), options);
Some(
match catch_unwind(|| {
format_path(path, &resolved_settings.formatter, source_type, mode)
}) {
Ok(inner) => inner,
Err(error) => {
Err(FormatCommandError::Panic(Some(path.to_path_buf()), error))
}
},
)
Some(match catch_unwind(|| format_path(path, options, mode)) {
Ok(inner) => inner,
Err(error) => {
Err(FormatCommandError::Panic(Some(path.to_path_buf()), error))
}
})
}
Err(err) => Some(Err(FormatCommandError::Ignore(err))),
}
@@ -141,15 +139,19 @@ pub(crate) fn format(
#[tracing::instrument(skip_all, fields(path = %path.display()))]
fn format_path(
path: &Path,
settings: &FormatterSettings,
source_type: PySourceType,
options: PyFormatOptions,
mode: FormatMode,
) -> Result<FormatCommandResult, FormatCommandError> {
let unformatted = std::fs::read_to_string(path)
.map_err(|err| FormatCommandError::Read(Some(path.to_path_buf()), err))?;
let options = settings.to_format_options(source_type, &unformatted);
debug!("Formatting {} with {:?}", path.display(), options);
let line_ending = match find_newline(&unformatted) {
Some((_, LineEnding::Lf)) | None => ruff_formatter::printer::LineEnding::LineFeed,
Some((_, LineEnding::Cr)) => ruff_formatter::printer::LineEnding::CarriageReturn,
Some((_, LineEnding::CrLf)) => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed,
};
let options = options.with_line_ending(line_ending);
let formatted = format_module(&unformatted, options)
.map_err(|err| FormatCommandError::FormatModule(Some(path.to_path_buf()), err))?;

View File

@@ -5,9 +5,8 @@ use anyhow::Result;
use log::warn;
use ruff_python_ast::PySourceType;
use ruff_python_formatter::format_module;
use ruff_python_formatter::{format_module, PyFormatOptions};
use ruff_workspace::resolver::python_file_at_path;
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
use crate::commands::format::{FormatCommandError, FormatCommandResult, FormatMode};
@@ -38,7 +37,12 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
// Format the file.
let path = cli.stdin_filename.as_deref();
match format_source(path, &pyproject_config.settings.formatter, mode) {
let options = pyproject_config
.settings
.formatter
.to_format_options(path.map(PySourceType::from).unwrap_or_default());
match format_source(path, options, mode) {
Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check => {
@@ -59,30 +63,23 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
/// Format source code read from `stdin`.
fn format_source(
path: Option<&Path>,
settings: &FormatterSettings,
options: PyFormatOptions,
mode: FormatMode,
) -> Result<FormatCommandResult, FormatCommandError> {
let unformatted = read_from_stdin()
.map_err(|err| FormatCommandError::Read(path.map(Path::to_path_buf), err))?;
let options = settings.to_format_options(
path.map(PySourceType::from).unwrap_or_default(),
&unformatted,
);
let formatted = format_module(&unformatted, options)
.map_err(|err| FormatCommandError::FormatModule(path.map(Path::to_path_buf), err))?;
let formatted = formatted.as_code();
if mode.is_write() {
stdout()
.lock()
.write_all(formatted.as_bytes())
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;
}
if formatted.len() == unformatted.len() && formatted == unformatted {
Ok(FormatCommandResult::Unchanged)
} else {
if mode.is_write() {
stdout()
.lock()
.write_all(formatted.as_bytes())
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;
}
Ok(FormatCommandResult::Formatted)
}
}

View File

@@ -1,207 +0,0 @@
#![cfg(not(target_family = "wasm"))]
use std::fs;
use std::process::Command;
use std::str;
use anyhow::Result;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
#[test]
fn default_options() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print('Should\'t change quotes')
if condition:
print('Hy "Micha"') # Should not change quotes
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(
arg1,
arg2,
):
print("Should't change quotes")
if condition:
print('Hy "Micha"') # Should not change quotes
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
"###);
}
#[test]
fn format_options() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[format]
indent-style = "tab"
quote-style = "single"
skip-magic-trailing-comma = true
line-ending = "cr-lf"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Shouldn't change quotes")
if condition:
print("Should change quotes")
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(arg1, arg2):
print("Shouldn't change quotes")
if condition:
print('Should change quotes')
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
"###);
Ok(())
}
#[test]
fn format_option_inheritance() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
let base_toml = tempdir.path().join("base.toml");
fs::write(
&ruff_toml,
r#"
extend = "base.toml"
[format]
quote-style = "single"
"#,
)?;
fs::write(
base_toml,
r#"
[format]
indent-style = "tab"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Shouldn't change quotes")
if condition:
print("Should change quotes")
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(
arg1,
arg2,
):
print("Shouldn't change quotes")
if condition:
print('Should change quotes')
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
"###);
Ok(())
}
/// Tests that the legacy `format` option continues to work but emits a warning.
#[test]
fn legacy_format_option() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
format = "json"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--select", "F401", "--no-cache", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
[
{
"code": "F401",
"end_location": {
"column": 10,
"row": 2
},
"filename": "-",
"fix": {
"applicability": "Automatic",
"edits": [
{
"content": "",
"end_location": {
"column": 1,
"row": 3
},
"location": {
"column": 1,
"row": 2
}
}
],
"message": "Remove unused import: `os`"
},
"location": {
"column": 8,
"row": 2
},
"message": "`os` imported but unused",
"noqa_row": 2,
"url": "https://docs.astral.sh/ruff/rules/unused-import"
}
]
----- stderr -----
warning: The option `format` has been deprecated to avoid ambiguity with Ruff's upcoming formatter. Use `output-format` instead.
"###);
Ok(())
}

View File

@@ -28,12 +28,12 @@ ruff_workspace = { path = "../ruff_workspace", features = ["schemars"]}
anyhow = { workspace = true }
clap = { workspace = true }
ignore = { workspace = true }
indicatif = "0.17.7"
indicatif = "0.17.5"
itertools = { workspace = true }
libcst = { workspace = true }
once_cell = { workspace = true }
pretty_assertions = { version = "1.3.0" }
rayon = "1.8.0"
rayon = "1.7.0"
regex = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }

View File

@@ -549,6 +549,7 @@ fn format_dir_entry(
let settings = resolver.resolve(&path, pyproject_config);
// That's a bad way of doing this but it's not worth doing something better for format_dev
// TODO(micha) use formatter settings instead
if settings.formatter.line_width != LineWidth::default() {
options = options.with_line_width(settings.formatter.line_width);
}

View File

@@ -11,7 +11,6 @@ use strum::IntoEnumIterator;
use ruff_diagnostics::AutofixKind;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
use crate::ROOT_DIR;
@@ -97,7 +96,10 @@ fn process_documentation(documentation: &str, out: &mut String) {
if let Some(rest) = line.strip_prefix("- `") {
let option = rest.trim_end().trim_end_matches('`');
assert!(Options::metadata().has(option), "unknown option {option}");
assert!(
Options::metadata().get(option).is_some(),
"unknown option {option}"
);
let anchor = option.replace('.', "-");
out.push_str(&format!("- [`{option}`][{option}]\n"));

View File

@@ -1,74 +1,9 @@
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
//!
//! Used for <https://docs.astral.sh/ruff/settings/>.
use std::fmt::Write;
use itertools::Itertools;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visit};
pub(crate) fn generate() -> String {
let mut output = String::new();
generate_set(&mut output, &Set::Toplevel(Options::metadata()));
output
}
fn generate_set(output: &mut String, set: &Set) {
writeln!(output, "### {title}\n", title = set.title()).unwrap();
if let Some(documentation) = set.metadata().documentation() {
output.push_str(documentation);
output.push('\n');
output.push('\n');
}
let mut visitor = CollectOptionsVisitor::default();
set.metadata().record(&mut visitor);
let (mut fields, mut sets) = (visitor.fields, visitor.groups);
fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
// Generate the fields.
for (name, field) in &fields {
emit_field(output, name, field, set.name());
output.push_str("---\n\n");
}
// Generate all the sub-sets.
for (set_name, sub_set) in &sets {
generate_set(output, &Set::Named(set_name, *sub_set));
}
}
enum Set<'a> {
Toplevel(OptionSet),
Named(&'a str, OptionSet),
}
impl<'a> Set<'a> {
fn name(&self) -> Option<&'a str> {
match self {
Set::Toplevel(_) => None,
Set::Named(name, _) => Some(name),
}
}
fn title(&self) -> &'a str {
match self {
Set::Toplevel(_) => "Top-level",
Set::Named(name, _) => name,
}
}
fn metadata(&self) -> &OptionSet {
match self {
Set::Toplevel(set) => set,
Set::Named(_, set) => set,
}
}
}
use ruff_workspace::options_base::{OptionEntry, OptionField};
fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name: Option<&str>) {
// if there's a group name, we need to add it to the anchor
@@ -102,18 +37,38 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name:
output.push('\n');
}
#[derive(Default)]
struct CollectOptionsVisitor {
groups: Vec<(String, OptionSet)>,
fields: Vec<(String, OptionField)>,
}
pub(crate) fn generate() -> String {
let mut output: String = "### Top-level\n\n".into();
impl Visit for CollectOptionsVisitor {
fn record_set(&mut self, name: &str, group: OptionSet) {
self.groups.push((name.to_owned(), group));
let sorted_options: Vec<_> = Options::metadata()
.into_iter()
.sorted_by_key(|(name, _)| *name)
.collect();
// Generate all the top-level fields.
for (name, entry) in &sorted_options {
let OptionEntry::Field(field) = entry else {
continue;
};
emit_field(&mut output, name, field, None);
output.push_str("---\n\n");
}
fn record_field(&mut self, name: &str, field: OptionField) {
self.fields.push((name.to_owned(), field));
// Generate all the sub-groups.
for (group_name, entry) in &sorted_options {
let OptionEntry::Group(fields) = entry else {
continue;
};
output.push_str(&format!("### {group_name}\n"));
output.push('\n');
for (name, entry) in fields.iter().sorted_by_key(|(name, _)| name) {
let OptionEntry::Field(field) = entry else {
continue;
};
emit_field(&mut output, name, field, Some(group_name));
output.push_str("---\n\n");
}
}
output
}

View File

@@ -9,7 +9,6 @@ use ruff_diagnostics::AutofixKind;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
const FIX_SYMBOL: &str = "🛠️";
const PREVIEW_SYMBOL: &str = "🧪";
@@ -105,7 +104,10 @@ pub(crate) fn generate() -> String {
table_out.push('\n');
}
if Options::metadata().has(linter.name()) {
if Options::metadata()
.iter()
.any(|(name, _)| name == &linter.name())
{
table_out.push_str(&format!(
"For related settings, see [{}](settings.md#{}).",
linter.name(),

View File

@@ -55,11 +55,7 @@ use ruff_macros::CacheKey;
use ruff_text_size::{TextRange, TextSize};
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, CacheKey)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default)]
pub enum IndentStyle {

View File

@@ -1,4 +1,5 @@
use crate::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
use ruff_macros::CacheKey;
/// Options that affect how the [`crate::Printer`] prints the format tokens
#[derive(Clone, Debug, Eq, PartialEq, Default)]
@@ -120,7 +121,7 @@ impl SourceMapGeneration {
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LineEnding {
/// Line Feed only (\n), common on Linux and macOS as well as inside git repos

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.0.291"
version = "0.0.290"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -104,12 +104,3 @@ def get_owner_id_from_mac_address():
mac_address = get_primary_mac_address()
except(IOError, OSError) as ex:
msg = 'Unable to query URL to get Owner ID: {u}\n{e}'.format(u=owner_id_url, e=ex)
# Regression test for: https://github.com/astral-sh/ruff/issues/7580
import os
try:
pass
except os.error:
pass

View File

@@ -695,7 +695,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
},
) => {
let module = module.as_deref();
let level = *level;
let level = level.map(|level| level.to_u32());
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
}

View File

@@ -358,7 +358,7 @@ where
range: _,
}) => {
let module = module.as_deref();
let level = *level;
let level = level.map(|level| level.to_u32());
for alias in names {
if let Some("__future__") = module {
let name = alias.asname.as_ref().unwrap_or(&alias.name);

View File

@@ -44,7 +44,7 @@ fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) ->
level,
range: _,
}) => {
let level = level.unwrap_or_default() as usize;
let level = level.map_or(0, |level| level.to_usize());
let module = if let Some(module) = module {
let module: &String = module.as_ref();
if level == 0 {
@@ -95,7 +95,6 @@ pub(crate) fn check_imports(
tracker.visit_body(python_ast);
tracker
};
let blocks: Vec<&Block> = tracker.iter().collect();
// Enforce import rules.

View File

@@ -308,7 +308,7 @@ impl<'a> Importer<'a> {
range: _,
}) = stmt
{
if level.map_or(true, |level| level == 0)
if level.map_or(true, |level| level.to_u32() == 0)
&& name.as_ref().is_some_and(|name| name == module)
&& names.iter().all(|alias| alias.name.as_str() != "*")
{

View File

@@ -21,25 +21,14 @@ use crate::checkers::ast::Checker;
/// `str.removesuffix` to remove an exact prefix or suffix from a string,
/// respectively, which should be preferred when possible.
///
/// ## Known problems
/// As a heuristic, this rule only flags multi-character strings that contain
/// duplicate characters. This allows usages like `.strip("xyz")`, which
/// removes all occurrences of the characters `x`, `y`, and `z` from the
/// leading and trailing ends of the string, but not `.strip("foo")`.
///
/// The use of unique, multi-character strings may be intentional and
/// consistent with the intent of `.strip()`, `.lstrip()`, or `.rstrip()`,
/// while the use of duplicate-character strings is very likely to be a
/// mistake.
///
/// ## Example
/// ```python
/// "text.txt".strip(".txt") # "ex"
/// "abcba".strip("ab") # "c"
/// ```
///
/// Use instead:
/// ```python
/// "text.txt".removesuffix(".txt") # "text"
/// "abcba".removeprefix("ab").removesuffix("ba") # "c"
/// ```
///
/// ## References
@@ -50,7 +39,7 @@ pub struct StripWithMultiCharacters;
impl Violation for StripWithMultiCharacters {
#[derive_message_formats]
fn message(&self) -> String {
format!("Using `.strip()` with multi-character strings is misleading")
format!("Using `.strip()` with multi-character strings is misleading the reader")
}
}
@@ -76,7 +65,8 @@ pub(crate) fn strip_with_multi_characters(
return;
};
if value.chars().count() > 1 && !value.chars().all_unique() {
let num_chars = value.chars().count();
if num_chars > 1 && num_chars != value.chars().unique().count() {
checker
.diagnostics
.push(Diagnostic::new(StripWithMultiCharacters, expr.range()));

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B005.py:4:1: B005 Using `.strip()` with multi-character strings is misleading
B005.py:4:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
2 | s.strip(s) # no warning
3 | s.strip("we") # no warning
@@ -11,7 +11,7 @@ B005.py:4:1: B005 Using `.strip()` with multi-character strings is misleading
6 | s.strip("\n\t ") # no warning
|
B005.py:7:1: B005 Using `.strip()` with multi-character strings is misleading
B005.py:7:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
5 | s.strip("e") # no warning
6 | s.strip("\n\t ") # no warning
@@ -21,7 +21,7 @@ B005.py:7:1: B005 Using `.strip()` with multi-character strings is misleading
9 | s.lstrip("we") # no warning
|
B005.py:10:1: B005 Using `.strip()` with multi-character strings is misleading
B005.py:10:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
8 | s.lstrip(s) # no warning
9 | s.lstrip("we") # no warning
@@ -31,7 +31,7 @@ B005.py:10:1: B005 Using `.strip()` with multi-character strings is misleading
12 | s.lstrip("\n\t ") # no warning
|
B005.py:13:1: B005 Using `.strip()` with multi-character strings is misleading
B005.py:13:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
11 | s.lstrip("e") # no warning
12 | s.lstrip("\n\t ") # no warning
@@ -41,7 +41,7 @@ B005.py:13:1: B005 Using `.strip()` with multi-character strings is misleading
15 | s.rstrip("we") # warning
|
B005.py:16:1: B005 Using `.strip()` with multi-character strings is misleading
B005.py:16:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
14 | s.rstrip(s) # no warning
15 | s.rstrip("we") # warning
@@ -51,7 +51,7 @@ B005.py:16:1: B005 Using `.strip()` with multi-character strings is misleading
18 | s.rstrip("\n\t ") # no warning
|
B005.py:19:1: B005 Using `.strip()` with multi-character strings is misleading
B005.py:19:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
17 | s.rstrip("e") # no warning
18 | s.rstrip("\n\t ") # no warning
@@ -61,7 +61,7 @@ B005.py:19:1: B005 Using `.strip()` with multi-character strings is misleading
21 | s.strip("あ") # no warning
|
B005.py:22:1: B005 Using `.strip()` with multi-character strings is misleading
B005.py:22:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
20 | s.strip("a") # no warning
21 | s.strip("あ") # no warning
@@ -71,7 +71,7 @@ B005.py:22:1: B005 Using `.strip()` with multi-character strings is misleading
24 | s.strip("\u0074\u0065\u0073\u0074") # warning
|
B005.py:24:1: B005 Using `.strip()` with multi-character strings is misleading
B005.py:24:1: B005 Using `.strip()` with multi-character strings is misleading the reader
|
22 | s.strip("ああ") # warning
23 | s.strip("\ufeff") # no warning

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Identifier, Stmt};
use ruff_python_ast::{self as ast, Identifier, Int, Stmt};
use ruff_text_size::{Ranged, TextRange};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
@@ -99,7 +99,7 @@ fn fix_banned_relative_import(
TextRange::default(),
)),
names: names.clone(),
level: Some(0),
level: Some(Int::new(0)),
range: TextRange::default(),
};
let content = generator.stmt(&node.into());

View File

@@ -118,7 +118,7 @@ pub(crate) fn annotate_imports<'a>(
AnnotatedImport::ImportFrom {
module: module.as_deref(),
names: aliases,
level: *level,
level: level.map(|level| level.to_u32()),
trailing_comma: if split_on_trailing_comma {
trailing_comma(import, locator, source_type)
} else {

View File

@@ -75,7 +75,7 @@ fn includes_import(stmt: &Stmt, target: &AnyImport) -> bool {
return false;
};
module.as_deref() == target.module
&& *level == target.level
&& level.map(|level| level.to_u32()) == target.level
&& names.iter().any(|alias| {
&alias.name == target.name.name
&& alias.asname.as_deref() == target.name.as_name
@@ -166,7 +166,7 @@ pub(crate) fn add_required_imports(
name: name.name.as_str(),
as_name: name.asname.as_deref(),
},
level: *level,
level: level.map(|level| level.to_u32()),
}),
python_ast,
locator,

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Alias, Identifier, Stmt};
use ruff_python_ast::{self as ast, Alias, Identifier, Int, Stmt};
use ruff_text_size::{Ranged, TextRange};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
@@ -80,7 +80,7 @@ pub(crate) fn manual_from_import(
asname: None,
range: TextRange::default(),
}],
level: Some(0),
level: Some(Int::new(0)),
range: TextRange::default(),
};
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(

View File

@@ -71,7 +71,7 @@ pub(crate) fn deprecated_c_element_tree(checker: &mut Checker, stmt: &Stmt) {
level,
range: _,
}) => {
if level.is_some_and(|level| level > 0) {
if level.is_some_and(|level| level.to_u32() > 0) {
// Ex) `import .xml.etree.cElementTree as ET`
} else if let Some(module) = module {
if module == "xml.etree.cElementTree" {

View File

@@ -323,7 +323,7 @@ pub(crate) fn deprecated_mock_import(checker: &mut Checker, stmt: &Stmt) {
level,
..
}) => {
if level.is_some_and(|level| level > 0) {
if level.is_some_and(|level| level.to_u32() > 0) {
return;
}

View File

@@ -61,7 +61,7 @@ fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool {
matches!(
call_path.as_slice(),
["", "EnvironmentError" | "IOError" | "WindowsError"]
| ["mmap" | "select" | "socket" | "os", "error"]
| ["mmap" | "select" | "socket", "error"]
)
})
}
@@ -93,13 +93,16 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
}
/// Create a [`Diagnostic`] for a tuple of expressions.
fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) {
let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, tuple.range());
fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) {
let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, target.range());
if checker.patch(diagnostic.kind.rule()) {
if checker.semantic().is_builtin("OSError") {
let Expr::Tuple(ast::ExprTuple { elts, .. }) = target else {
panic!("Expected Expr::Tuple");
};
// Filter out any `OSErrors` aliases.
let mut remaining: Vec<Expr> = tuple
.elts
let mut remaining: Vec<Expr> = elts
.iter()
.filter_map(|elt| {
if aliases.contains(&elt) {
@@ -111,11 +114,7 @@ fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&E
.collect();
// If `OSError` itself isn't already in the tuple, add it.
if tuple
.elts
.iter()
.all(|elt| !is_os_error(elt, checker.semantic()))
{
if elts.iter().all(|elt| !is_os_error(elt, checker.semantic())) {
let node = ast::ExprName {
id: "OSError".into(),
ctx: ExprContext::Load,
@@ -136,8 +135,8 @@ fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&E
};
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
pad(content, tuple.range(), checker.locator()),
tuple.range(),
pad(content, target.range(), checker.locator()),
target.range(),
)));
}
}
@@ -157,16 +156,16 @@ pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[ExceptH
atom_diagnostic(checker, expr);
}
}
Expr::Tuple(tuple) => {
Expr::Tuple(ast::ExprTuple { elts, .. }) => {
// List of aliases to replace with `OSError`.
let mut aliases: Vec<&Expr> = vec![];
for elt in &tuple.elts {
for elt in elts {
if is_alias(elt, checker.semantic()) {
aliases.push(elt);
}
}
if !aliases.is_empty() {
tuple_diagnostic(checker, tuple, &aliases);
tuple_diagnostic(checker, expr, &aliases);
}
}
_ => {}

View File

@@ -165,7 +165,7 @@ fn compare_version(target_version: &[u32], py_version: PythonVersion, or_equal:
let (py_major, py_minor) = py_version.as_tuple();
match if_major.cmp(&py_major.into()) {
match if_major.cmp(&py_major) {
Ordering::Less => true,
Ordering::Greater => false,
Ordering::Equal => {
@@ -175,11 +175,11 @@ fn compare_version(target_version: &[u32], py_version: PythonVersion, or_equal:
if or_equal {
// Ex) `sys.version_info <= 3.8`. If Python 3.8 is the minimum supported version,
// the condition won't always evaluate to `false`, so we want to return `false`.
*if_minor < py_minor.into()
*if_minor < py_minor
} else {
// Ex) `sys.version_info < 3.8`. If Python 3.8 is the minimum supported version,
// the condition _will_ always evaluate to `false`, so we want to return `true`.
*if_minor <= py_minor.into()
*if_minor <= py_minor
}
}
}

View File

@@ -281,25 +281,5 @@ UP024_0.py:105:11: UP024 [*] Replace aliased errors with `OSError`
105 |- except(IOError, OSError) as ex:
105 |+ except OSError as ex:
106 106 | msg = 'Unable to query URL to get Owner ID: {u}\n{e}'.format(u=owner_id_url, e=ex)
107 107 |
108 108 |
UP024_0.py:114:8: UP024 [*] Replace aliased errors with `OSError`
|
112 | try:
113 | pass
114 | except os.error:
| ^^^^^^^^ UP024
115 | pass
|
= help: Replace `os.error` with builtin `OSError`
Fix
111 111 |
112 112 | try:
113 113 | pass
114 |-except os.error:
114 |+except OSError:
115 115 | pass

View File

@@ -11,7 +11,7 @@ use ruff_source_file::Locator;
///
/// A known type is either a builtin type, any object from the standard library,
/// or a type from the `typing_extensions` module.
fn is_known_type(call_path: &CallPath, minor_version: u8) -> bool {
fn is_known_type(call_path: &CallPath, minor_version: u32) -> bool {
match call_path.as_slice() {
["" | "typing_extensions", ..] => true,
[module, ..] => is_known_standard_library(minor_version, module),
@@ -72,7 +72,7 @@ impl<'a> TypingTarget<'a> {
expr: &'a Expr,
semantic: &SemanticModel,
locator: &Locator,
minor_version: u8,
minor_version: u32,
) -> Option<Self> {
match expr {
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
@@ -141,7 +141,7 @@ impl<'a> TypingTarget<'a> {
&self,
semantic: &SemanticModel,
locator: &Locator,
minor_version: u8,
minor_version: u32,
) -> bool {
match self {
TypingTarget::None
@@ -189,7 +189,12 @@ impl<'a> TypingTarget<'a> {
}
/// Check if the [`TypingTarget`] explicitly allows `Any`.
fn contains_any(&self, semantic: &SemanticModel, locator: &Locator, minor_version: u8) -> bool {
fn contains_any(
&self,
semantic: &SemanticModel,
locator: &Locator,
minor_version: u32,
) -> bool {
match self {
TypingTarget::Any => true,
// `Literal` cannot contain `Any` as it's a dynamic value.
@@ -237,7 +242,7 @@ pub(crate) fn type_hint_explicitly_allows_none<'a>(
annotation: &'a Expr,
semantic: &SemanticModel,
locator: &Locator,
minor_version: u8,
minor_version: u32,
) -> Option<&'a Expr> {
match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) {
None |
@@ -267,7 +272,7 @@ pub(crate) fn type_hint_resolves_to_any(
annotation: &Expr,
semantic: &SemanticModel,
locator: &Locator,
minor_version: u8,
minor_version: u32,
) -> bool {
match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) {
None |

View File

@@ -58,7 +58,7 @@ impl PythonVersion {
Self::Py312
}
pub const fn as_tuple(&self) -> (u8, u8) {
pub const fn as_tuple(&self) -> (u32, u32) {
match self {
Self::Py37 => (3, 7),
Self::Py38 => (3, 8),
@@ -69,11 +69,11 @@ impl PythonVersion {
}
}
pub const fn major(&self) -> u8 {
pub const fn major(&self) -> u32 {
self.as_tuple().0
}
pub const fn minor(&self) -> u8 {
pub const fn minor(&self) -> u32 {
self.as_tuple().1
}

View File

@@ -10,12 +10,7 @@ use syn::{
};
pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let DeriveInput {
ident,
data,
attrs: struct_attributes,
..
} = input;
let DeriveInput { ident, data, .. } = input;
match data {
Data::Struct(DataStruct {
@@ -55,39 +50,15 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenS
};
}
let docs: Vec<&Attribute> = struct_attributes
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.collect();
// Convert the list of `doc` attributes into a single string.
let doc = dedent(
&docs
.into_iter()
.map(parse_doc)
.collect::<syn::Result<Vec<_>>>()?
.join("\n"),
)
.trim_matches('\n')
.to_string();
let documentation = if doc.is_empty() {
None
} else {
Some(quote!(
fn documentation() -> Option<&'static str> {
Some(&#doc)
}
))
};
let options_len = output.len();
Ok(quote! {
impl crate::options_base::OptionsMetadata for #ident {
fn record(visit: &mut dyn crate::options_base::Visit) {
#(#output);*
}
#documentation
impl #ident {
pub const fn metadata() -> crate::options_base::OptionGroup {
const OPTIONS: [(&'static str, crate::options_base::OptionEntry); #options_len] = [#(#output),*];
crate::options_base::OptionGroup::new(&OPTIONS)
}
}
})
}
@@ -121,7 +92,7 @@ fn handle_option_group(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
Ok(quote_spanned!(
ident.span() => (visit.record_set(#kebab_name, crate::options_base::OptionSet::of::<#path>()))
ident.span() => (#kebab_name, crate::options_base::OptionEntry::Group(#path::metadata()))
))
}
_ => Err(syn::Error::new(
@@ -179,14 +150,12 @@ fn handle_option(
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
Ok(quote_spanned!(
ident.span() => {
visit.record_field(#kebab_name, crate::options_base::OptionField{
doc: &#doc,
default: &#default,
value_type: &#value_type,
example: &#example,
})
}
ident.span() => (#kebab_name, crate::options_base::OptionEntry::Field(crate::options_base::OptionField {
doc: &#doc,
default: &#default,
value_type: &#value_type,
example: &#example,
}))
))
}

View File

@@ -1161,7 +1161,7 @@ pub struct StmtImport<'a> {
pub struct StmtImportFrom<'a> {
module: Option<&'a str>,
names: Vec<ComparableAlias<'a>>,
level: Option<u32>,
level: Option<ast::Int>,
}
#[derive(Debug, PartialEq, Eq, Hash)]

View File

@@ -466,7 +466,7 @@ pub struct StmtImportFrom {
pub range: TextRange,
pub module: Option<Identifier>,
pub names: Vec<Alias>,
pub level: Option<u32>,
pub level: Option<Int>,
}
impl From<StmtImportFrom> for Stmt {
@@ -2578,6 +2578,35 @@ impl Ranged for Identifier {
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Int(u32);
impl Int {
pub fn new(i: u32) -> Self {
Self(i)
}
pub fn to_u32(&self) -> u32 {
self.0
}
pub fn to_usize(&self) -> usize {
self.0 as _
}
}
impl std::cmp::PartialEq<u32> for Int {
#[inline]
fn eq(&self, other: &u32) -> bool {
self.0 == *other
}
}
impl std::cmp::PartialEq<usize> for Int {
#[inline]
fn eq(&self, other: &usize) -> bool {
self.0 as usize == *other
}
}
#[derive(Clone, Debug, PartialEq, is_macro::Is)]
pub enum Constant {
None,

View File

@@ -577,9 +577,7 @@ impl<'a> Generator<'a> {
statement!({
self.p("from ");
if let Some(level) = level {
for _ in 0..*level {
self.p(".");
}
self.p(&".".repeat(level.to_usize()));
}
if let Some(module) = module {
self.p_id(module);

View File

@@ -30,7 +30,6 @@ memchr = { workspace = true }
once_cell = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true, optional = true }
schemars = { workspace = true, optional = true }
smallvec = { workspace = true }
static_assertions = { workspace = true }
thiserror = { workspace = true }
@@ -53,5 +52,4 @@ required-features = ["serde"]
[features]
serde = ["dep:serde", "ruff_formatter/serde", "ruff_source_file/serde", "ruff_python_ast/serde"]
schemars = ["dep:schemars", "ruff_formatter/schemars"]
default = []
default = ["serde"]

View File

@@ -1,18 +1,18 @@
[
{
"indent_style": "space",
"indent_style": "Space",
"indent_width": 4
},
{
"indent_style": "space",
"indent_style": "Space",
"indent_width": 2
},
{
"indent_style": "tab",
"indent_style": "Tab",
"indent_width": 8
},
{
"indent_style": "tab",
"indent_style": "Tab",
"indent_width": 4
}
]

View File

@@ -1,10 +1,10 @@
[
{
"indent_style": "space",
"indent_style": "Space",
"indent_width": 4
},
{
"indent_style": "space",
"indent_style": "Space",
"indent_width": 2
}
]

View File

@@ -1,13 +1,13 @@
[
{
"indent_style": "space",
"indent_style": "Space",
"indent_width": 4
},
{
"indent_style": "space",
"indent_style": "Space",
"indent_width": 1
},
{
"indent_style": "tab"
"indent_style": "Tab"
}
]

View File

@@ -1,13 +1,13 @@
[
{
"indent_style": "space",
"indent_style": "Space",
"indent_width": 4
},
{
"indent_style": "space",
"indent_style": "Space",
"indent_width": 2
},
{
"indent_style": "tab"
"indent_style": "Tab"
}
]

View File

@@ -1,161 +0,0 @@
###
# Blank lines around functions
###
x = 1
# comment
def f():
pass
if True:
x = 1
# comment
def f():
pass
x = 1
# comment
def f():
pass
x = 1
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
# comment
def f():
pass
# comment
def f():
pass
# comment
###
# Blank lines around imports.
###
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x # comment
# comment
import y
def f(): pass # comment
# comment
x = 1
def f():
pass
# comment
x = 1

View File

@@ -376,11 +376,3 @@ def f( # first
def this_is_unusual() -> (please := no): ...
def this_is_unusual(x) -> (please := no): ...
# Regression test for: https://github.com/astral-sh/ruff/issues/7465
try:
def test():
pass
#comment
except ImportError:
pass

View File

@@ -104,110 +104,6 @@ if True: print("a") # 1
elif True: print("b") # 2
else: print("c") # 3
# Regression test for: https://github.com/astral-sh/ruff/issues/7465
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass # comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
if True:
pass
# comment
else:
pass
else:
pass
if True:
if True:
pass
# comment
else:
pass
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
# Regression test for https://github.com/astral-sh/ruff/issues/5337
if parent_body:
if current_body:

View File

@@ -3,7 +3,7 @@ use std::borrow::Cow;
use ruff_formatter::{format_args, write, FormatError, FormatOptions, SourceCode};
use ruff_python_ast::node::{AnyNodeRef, AstNode};
use ruff_python_ast::PySourceType;
use ruff_python_trivia::{lines_after, lines_before};
use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before};
use ruff_text_size::{Ranged, TextLen, TextRange};
use crate::comments::{CommentLinePosition, SourceComment};
@@ -86,26 +86,17 @@ impl Format<PyFormatContext<'_>> for FormatLeadingAlternateBranchComments<'_> {
if let Some(first_leading) = self.comments.first() {
// Leading comments only preserves the lines after the comment but not before.
// Insert the necessary lines.
write!(
f,
[empty_lines(lines_before(
first_leading.start(),
f.context().source()
))]
)?;
if lines_before(first_leading.start(), f.context().source()) > 1 {
write!(f, [empty_line()])?;
}
write!(f, [leading_comments(self.comments)])?;
} else if let Some(last_preceding) = self.last_node {
// The leading comments formatting ensures that it preserves the right amount of lines after
// We need to take care of this ourselves, if there's no leading `else` comment.
let end = if let Some(last_trailing) =
f.context().comments().trailing(last_preceding).last()
{
last_trailing.end()
} else {
last_preceding.end()
};
write!(f, [empty_lines(lines_after(end, f.context().source()))])?;
if lines_after_ignoring_trivia(last_preceding.end(), f.context().source()) > 1 {
write!(f, [empty_line()])?;
}
}
Ok(())
@@ -308,14 +299,7 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLines {
NodeLevel::TopLevel => match self.lines {
0 | 1 => write!(f, [hard_line_break()]),
2 => write!(f, [empty_line()]),
_ => match f.options().source_type() {
PySourceType::Stub => {
write!(f, [empty_line()])
}
PySourceType::Python | PySourceType::Ipynb => {
write!(f, [empty_line(), empty_line()])
}
},
_ => write!(f, [empty_line(), empty_line()]),
},
NodeLevel::CompoundStatement => match self.lines {

View File

@@ -1,4 +1,4 @@
use ruff_formatter::FormatRuleWithOptions;
use ruff_formatter::{FormatContext, FormatRuleWithOptions};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{Constant, ExprConstant};
use ruff_text_size::{Ranged, TextLen, TextRange};
@@ -78,6 +78,8 @@ impl NeedsParentheses for ExprConstant {
OptionalParentheses::Multiline
} else if is_multiline_string(self, context.source()) {
OptionalParentheses::Never
} else if context.options().preview().is_enabled() {
OptionalParentheses::Multiline
} else {
OptionalParentheses::BestFit
}

View File

@@ -1,6 +1,7 @@
use std::borrow::Cow;
use bitflags::bitflags;
use smallvec::SmallVec;
use ruff_formatter::{format_args, write, FormatError};
use ruff_python_ast::node::AnyNodeRef;
@@ -139,18 +140,131 @@ impl<'a> FormatString<'a> {
impl<'a> Format<PyFormatContext<'_>> for FormatString<'a> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
let locator = f.context().locator();
let quote_style = f.options().quote_style();
match self.layout {
StringLayout::Default => {
if self.string.is_implicit_concatenated() {
in_parentheses_only_group(&FormatStringContinuation::new(self.string)).fmt(f)
let continuation = StringContinuation::from_string(self.string, &locator)?;
if f.options().preview().is_enabled() {
if let Some((first, rest)) = continuation.parts.split_first() {
let first_normalized =
first.normalize(Quoting::CanChange, &locator, quote_style);
let quotes = first_normalized.quotes;
let mut normalized = SmallVec::with_capacity(continuation.parts.len());
normalized.push(first_normalized);
for part in rest {
normalized.push(part.normalize_with_quotes(&locator, quotes));
}
let continuation = NormalizedStringContinuation {
string: self.string,
parts: normalized,
};
let format_flat = format_with(|f| {
quotes.fmt(f)?;
// TODO comments
for part in &continuation.parts {
match &part.text {
Cow::Borrowed(_) => {
source_text_slice(part.range()).fmt(f)?
}
Cow::Owned(content) => {
text(&content, Some(part.start())).fmt(f)?
}
}
}
quotes.fmt(f)
});
let format_multiline = format_with(|f| {
// TODO won't format comments again
group(&continuation).should_expand(true).fmt(f)
});
let format_joined = format_with(|f| {
quotes.fmt(f)?;
let mut fill = f.fill();
let separator = format_with(|f| {
group(&format_args![
if_group_breaks(&quotes),
soft_line_break_or_space(),
if_group_breaks(&format_args![quotes, space()])
])
.fmt(f)
});
for part in &continuation.parts {
let mut words = part.text.split(' ').peekable();
while let Some(word) = words.next() {
let is_last = words.peek().is_none();
let format_word =
format_with(|f| write!(f, [text(word, None)]));
fill.entry(&separator, &format_word);
}
}
fill.finish()?;
quotes.fmt(f)
});
best_fitting![format_flat, format_multiline, format_joined]
.with_mode(BestFittingMode::AllLines)
.fmt(f)?;
}
Ok(())
} else {
in_parentheses_only_group(&continuation.normalize(quote_style, &locator))
.fmt(f)
}
} else {
StringPart::from_source(self.string.range(), &locator)
.normalize(
self.string.quoting(&locator),
&locator,
f.options().quote_style(),
)
.fmt(f)
// Joining/ splitting does not apply to triple quoted strings or expression statement strings
// Splitting only applies in parenthesized contexts
// Joins strings in non parenthesized contexts
// Does not join/split if string has a prefix other than `u` or `f`.
if f.options().preview().is_enabled() {
let normalized = StringPart::from_source(self.string.range(), &locator)
.normalize(self.string.quoting(&locator), &locator, quote_style);
// TODO how to optimize to avoid allocating a string for every word?
write!(f, [normalized.prefix, normalized.quotes])?;
// TODO split by words longer with a length of at least 6 characters.
let mut words = normalized.text.split(' ');
let mut fill = f.fill();
let separator = format_with(|f| {
group(&format_args![
if_group_breaks(&normalized.quotes),
soft_line_break_or_space(),
if_group_breaks(&format_args![normalized.quotes, space()])
])
.fmt(f)
});
while let Some(word) = words.next() {
let format_word = format_with(|f| write!(f, [text(word, None)]));
fill.entry(&separator, &format_word);
}
fill.finish()?;
normalized.quotes.fmt(f)
} else {
StringPart::from_source(self.string.range(), &locator)
.normalize(self.string.quoting(&locator), &locator, quote_style)
.fmt(f)
}
}
}
StringLayout::DocString => {
@@ -160,33 +274,24 @@ impl<'a> Format<PyFormatContext<'_>> for FormatString<'a> {
format_docstring(&normalized, f)
}
StringLayout::ImplicitConcatenatedStringInBinaryLike => {
FormatStringContinuation::new(self.string).fmt(f)
StringContinuation::from_string(self.string, &locator)?
.normalize(f.options().quote_style(), &locator)
.fmt(f)
}
}
}
}
struct FormatStringContinuation<'a> {
struct StringContinuation<'a> {
parts: SmallVec<[StringPart; 4]>,
string: &'a AnyString<'a>,
}
impl<'a> FormatStringContinuation<'a> {
fn new(string: &'a AnyString<'a>) -> Self {
if let AnyString::Constant(constant) = string {
debug_assert!(constant.value.is_str() || constant.value.is_bytes());
}
Self { string }
}
}
impl<'a> StringContinuation<'a> {
fn from_string(string: &'a AnyString<'a>, locator: &Locator) -> FormatResult<Self> {
debug_assert!(string.is_implicit_concatenated());
impl Format<PyFormatContext<'_>> for FormatStringContinuation<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
let comments = f.context().comments().clone();
let locator = f.context().locator();
let quote_style = f.options().quote_style();
let mut dangling_comments = comments.dangling(self.string);
let string_range = self.string.range();
let string_range = string.range();
let string_content = locator.slice(string_range);
// The AST parses implicit concatenation as a single string.
@@ -195,7 +300,7 @@ impl Format<PyFormatContext<'_>> for FormatStringContinuation<'_> {
// because this is a black preview style.
let lexer = lex_starts_at(string_content, Mode::Expression, string_range.start());
let mut joiner = f.join_with(in_parentheses_only_soft_line_break_or_space());
let mut parts = SmallVec::new();
for token in lexer {
let (token, token_range) = match token {
@@ -228,46 +333,7 @@ impl Format<PyFormatContext<'_>> for FormatStringContinuation<'_> {
match token {
Tok::String { .. } => {
// ```python
// (
// "a"
// # leading
// "the comment above"
// )
// ```
let leading_comments_end = dangling_comments
.partition_point(|comment| comment.start() <= token_range.start());
let (leading_part_comments, rest) =
dangling_comments.split_at(leading_comments_end);
// ```python
// (
// "a" # trailing comment
// "the comment above"
// )
// ```
let trailing_comments_end = rest.partition_point(|comment| {
comment.line_position().is_end_of_line()
&& !locator.contains_line_break(TextRange::new(
token_range.end(),
comment.start(),
))
});
let (trailing_part_comments, rest) = rest.split_at(trailing_comments_end);
let part = StringPart::from_source(token_range, &locator);
let normalized =
part.normalize(self.string.quoting(&locator), &locator, quote_style);
joiner.entry(&format_args![
line_suffix_boundary(),
leading_comments(leading_part_comments),
normalized,
trailing_comments(trailing_part_comments)
]);
dangling_comments = rest;
parts.push(StringPart::from_source(token_range, locator));
}
Tok::Comment(_)
| Tok::NonLogicalNewline
@@ -278,6 +344,79 @@ impl Format<PyFormatContext<'_>> for FormatStringContinuation<'_> {
}
}
Ok(Self { parts, string })
}
fn normalize(
self,
quote_style: QuoteStyle,
locator: &Locator<'a>,
) -> NormalizedStringContinuation<'a> {
let quoting = self.string.quoting(locator);
let normalized = self
.parts
.into_iter()
.map(|part| part.normalize(quoting, &locator, quote_style))
.collect();
NormalizedStringContinuation {
parts: normalized,
string: self.string,
}
}
}
struct NormalizedStringContinuation<'a> {
parts: SmallVec<[NormalizedString<'a>; 4]>,
string: &'a AnyString<'a>,
}
impl Format<PyFormatContext<'_>> for NormalizedStringContinuation<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
let comments = f.context().comments().clone();
let locator = f.context().locator();
let quote_style = f.options().quote_style();
let quoting = self.string.quoting(&locator);
let mut dangling_comments = comments.dangling(self.string);
let mut joiner = f.join_with(in_parentheses_only_soft_line_break_or_space());
for part in &self.parts {
// ```python
// (
// "a"
// # leading
// "the comment above"
// )
// ```
let leading_comments_end =
dangling_comments.partition_point(|comment| comment.start() <= part.start());
let (leading_part_comments, rest) = dangling_comments.split_at(leading_comments_end);
// ```python
// (
// "a" # trailing comment
// "the comment above"
// )
// ```
let trailing_comments_end = rest.partition_point(|comment| {
comment.line_position().is_end_of_line()
&& !locator.contains_line_break(TextRange::new(part.end(), comment.start()))
});
let (trailing_part_comments, rest) = rest.split_at(trailing_comments_end);
joiner.entry(&format_args![
line_suffix_boundary(),
leading_comments(leading_part_comments),
part,
trailing_comments(trailing_part_comments)
]);
dangling_comments = rest;
}
debug_assert!(dangling_comments.is_empty());
joiner.finish()
@@ -320,10 +459,10 @@ impl StringPart {
/// Computes the strings preferred quotes and normalizes its content.
fn normalize<'a>(
self,
&self,
quoting: Quoting,
locator: &'a Locator,
quote_style: QuoteStyle,
locator: &Locator<'a>,
configured_quote_style: QuoteStyle,
) -> NormalizedString<'a> {
let raw_content = locator.slice(self.content_range);
@@ -331,28 +470,40 @@ impl StringPart {
Quoting::Preserve => self.quotes,
Quoting::CanChange => {
if self.prefix.is_raw_string() {
preferred_quotes_raw(raw_content, self.quotes, quote_style)
preferred_quotes_raw(raw_content, self.quotes, configured_quote_style)
} else {
preferred_quotes(raw_content, self.quotes, quote_style)
preferred_quotes(raw_content, self.quotes, configured_quote_style)
}
}
};
let normalized = normalize_string(
locator.slice(self.content_range),
preferred_quotes,
self.prefix.is_raw_string(),
);
self.normalize_with_quotes(locator, preferred_quotes)
}
fn normalize_with_quotes<'a>(
&self,
locator: &Locator<'a>,
quotes: StringQuotes,
) -> NormalizedString<'a> {
let raw_content = locator.slice(self.content_range);
let normalized = normalize_string(raw_content, quotes, self.prefix.is_raw_string());
NormalizedString {
prefix: self.prefix,
content_range: self.content_range,
text: normalized,
quotes: preferred_quotes,
quotes,
}
}
}
impl Ranged for StringPart {
fn range(&self) -> TextRange {
self.content_range
}
}
#[derive(Debug)]
struct NormalizedString<'a> {
prefix: StringPrefix,

View File

@@ -17,6 +17,7 @@ use crate::comments::{
pub use crate::context::PyFormatContext;
pub use crate::options::{MagicTrailingComma, PreviewMode, PyFormatOptions, QuoteStyle};
use crate::verbatim::suppressed_node;
pub use settings::FormatterSettings;
pub(crate) mod builders;
pub mod cli;
@@ -29,6 +30,7 @@ mod options;
pub(crate) mod other;
pub(crate) mod pattern;
mod prelude;
mod settings;
pub(crate) mod statement;
pub(crate) mod type_param;
mod verbatim;

View File

@@ -5,8 +5,8 @@ use ruff_python_ast::PySourceType;
use std::path::Path;
use std::str::FromStr;
/// Resolved options for formatting one individual file. The difference to `FormatterSettings`
/// is that `FormatterSettings` stores the settings for multiple files (the entire project, a subdirectory, ..)
/// Resolved options for formatting one individual file. This is different from [`crate::FormatterSettings`] which
/// represents the formatting settings for multiple files (the whole project, a subdirectory, ...)
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serde",
@@ -185,7 +185,6 @@ impl FormatOptions for PyFormatOptions {
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum QuoteStyle {
Single,
#[default]
@@ -269,9 +268,9 @@ impl FromStr for MagicTrailingComma {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum PreviewMode {
#[default]
Disabled,
#[default]
Enabled,
}

View File

@@ -0,0 +1,49 @@
use std::path::PathBuf;
use ruff_formatter::{FormatOptions, IndentStyle, LineWidth};
use ruff_macros::CacheKey;
use ruff_python_ast::PySourceType;
use crate::{MagicTrailingComma, PreviewMode, PyFormatOptions, QuoteStyle};
#[derive(CacheKey, Clone, Debug)]
pub struct FormatterSettings {
/// The files that are excluded from formatting (but may be linted).
pub exclude: Vec<PathBuf>,
pub preview: PreviewMode,
pub line_width: LineWidth,
pub indent_style: IndentStyle,
pub quote_style: QuoteStyle,
pub magic_trailing_comma: MagicTrailingComma,
}
impl FormatterSettings {
pub fn to_format_options(&self, source_type: PySourceType) -> PyFormatOptions {
PyFormatOptions::from_source_type(source_type)
.with_indent_style(self.indent_style)
.with_quote_style(self.quote_style)
.with_magic_trailing_comma(self.magic_trailing_comma)
.with_preview(self.preview)
.with_line_width(self.line_width)
}
}
impl Default for FormatterSettings {
fn default() -> Self {
let default_options = PyFormatOptions::default();
Self {
exclude: Vec::default(),
preview: PreviewMode::Disabled,
line_width: default_options.line_width(),
indent_style: default_options.indent_style(),
quote_style: default_options.quote_style(),
magic_trailing_comma: default_options.magic_trailing_comma(),
}
}
}

View File

@@ -21,7 +21,7 @@ impl FormatNodeRule<StmtImportFrom> for FormatStmtImportFrom {
} = item;
let level_str = level
.map(|level| ".".repeat(level as usize))
.map(|level| ".".repeat(level.to_usize()))
.unwrap_or(String::default());
write!(

View File

@@ -1,7 +1,7 @@
use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions};
use ruff_python_ast::helpers::is_compound_statement;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{self as ast, Constant, Expr, ExprConstant, PySourceType, Stmt, Suite};
use ruff_python_ast::{self as ast, Constant, Expr, ExprConstant, Stmt, Suite};
use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before};
use ruff_text_size::{Ranged, TextRange};
@@ -192,14 +192,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
SuiteKind::TopLevel => {
match lines_after_ignoring_trivia(preceding.end(), source) {
0..=2 => empty_line().fmt(f)?,
_ => match source_type {
PySourceType::Stub => {
empty_line().fmt(f)?;
}
PySourceType::Python | PySourceType::Ipynb => {
write!(f, [empty_line(), empty_line()])?;
}
},
_ => write!(f, [empty_line(), empty_line()])?,
}
}
SuiteKind::Function | SuiteKind::Class | SuiteKind::Other => {
@@ -232,15 +225,8 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
match lines_before(start, source) {
0 | 1 => hard_line_break().fmt(f)?,
2 => empty_line().fmt(f)?,
_ => match self.kind {
SuiteKind::TopLevel => match source_type {
PySourceType::Stub => {
empty_line().fmt(f)?;
}
PySourceType::Python | PySourceType::Ipynb => {
write!(f, [empty_line(), empty_line()])?;
}
},
3.. => match self.kind {
SuiteKind::TopLevel => write!(f, [empty_line(), empty_line()])?,
SuiteKind::Function | SuiteKind::Class | SuiteKind::Other => {
empty_line().fmt(f)?;
}
@@ -294,14 +280,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
NodeLevel::TopLevel => match count_lines(end) {
0 | 1 => hard_line_break().fmt(f)?,
2 => empty_line().fmt(f)?,
_ => match source_type {
PySourceType::Stub => {
empty_line().fmt(f)?;
}
PySourceType::Python | PySourceType::Ipynb => {
write!(f, [empty_line(), empty_line()])?;
}
},
_ => write!(f, [empty_line(), empty_line()])?,
},
NodeLevel::CompoundStatement => match count_lines(end) {
0 | 1 => hard_line_break().fmt(f)?,

View File

@@ -1,311 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.pyi
---
## Input
```py
###
# Blank lines around functions
###
x = 1
# comment
def f():
pass
if True:
x = 1
# comment
def f():
pass
x = 1
# comment
def f():
pass
x = 1
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
# comment
def f():
pass
# comment
def f():
pass
# comment
###
# Blank lines around imports.
###
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x # comment
# comment
import y
def f(): pass # comment
# comment
x = 1
def f():
pass
# comment
x = 1
```
## Output
```py
###
# Blank lines around functions
###
x = 1
# comment
def f():
pass
if True:
x = 1
# comment
def f():
pass
x = 1
# comment
def f():
pass
x = 1
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
x = 1
# comment
# comment
def f():
pass
# comment
def f():
pass
# comment
def f():
pass
# comment
###
# Blank lines around imports.
###
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x
# comment
import y
def f():
import x # comment
# comment
import y
def f():
pass # comment
# comment
x = 1
def f():
pass
# comment
x = 1
```

View File

@@ -382,14 +382,6 @@ def f( # first
def this_is_unusual() -> (please := no): ...
def this_is_unusual(x) -> (please := no): ...
# Regression test for: https://github.com/astral-sh/ruff/issues/7465
try:
def test():
pass
#comment
except ImportError:
pass
```
## Output
@@ -929,17 +921,6 @@ def this_is_unusual() -> (please := no):
def this_is_unusual(x) -> (please := no):
...
# Regression test for: https://github.com/astral-sh/ruff/issues/7465
try:
def test():
pass
# comment
except ImportError:
pass
```

View File

@@ -110,110 +110,6 @@ if True: print("a") # 1
elif True: print("b") # 2
else: print("c") # 3
# Regression test for: https://github.com/astral-sh/ruff/issues/7465
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass # comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
if True:
pass
# comment
else:
pass
else:
pass
if True:
if True:
pass
# comment
else:
pass
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
# Regression test for https://github.com/astral-sh/ruff/issues/5337
if parent_body:
if current_body:
@@ -267,7 +163,6 @@ elif aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
]:
...
else:
...
@@ -342,108 +237,6 @@ elif True:
else:
print("c") # 3
# Regression test for: https://github.com/astral-sh/ruff/issues/7465
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass # comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
if True:
pass
# comment
else:
pass
else:
pass
if True:
if True:
pass
# comment
else:
pass
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
if True:
pass
# comment
else:
pass
# Regression test for https://github.com/astral-sh/ruff/issues/5337
if parent_body:
if current_body:

View File

@@ -253,18 +253,18 @@ ImportStatement: ast::Stmt = {
},
};
ImportFromLocation: (Option<u32>, Option<ast::Identifier>) = {
ImportFromLocation: (Option<ast::Int>, Option<ast::Identifier>) = {
<dots: ImportDots*> <name:DottedName> => {
(Some(dots.iter().sum()), Some(name))
(Some(ast::Int::new(dots.iter().map(ast::Int::to_u32).sum())), Some(name))
},
<dots: ImportDots+> => {
(Some(dots.iter().sum()), None)
(Some(ast::Int::new(dots.iter().map(ast::Int::to_u32).sum())), None)
},
};
ImportDots: u32 = {
"..." => 3,
"." => 1,
ImportDots: ast::Int = {
"..." => ast::Int::new(3),
"." => ast::Int::new(1),
};
ImportAsNames: Vec<ast::Alias> = {

View File

@@ -1,5 +1,5 @@
// auto-generated: "lalrpop 0.20.0"
// sha3: eb535c9ae34baad8c940ef61dbbea0a7fec7baf3cd62af40837b2616f656f927
// sha3: e8f3229288c1a13387ea6041355e2d8fe9ab788fbc7229032d2de92beb675944
use num_bigint::BigInt;
use ruff_text_size::{Ranged, TextSize};
use ruff_python_ast::{self as ast, IpyEscapeKind};
@@ -117,9 +117,9 @@ mod __parse__Top {
Variant69(core::option::Option<(Option<(TextSize, TextSize, Option<ast::Identifier>)>, ast::Expr)>),
Variant70(ast::Alias),
Variant71(Vec<ast::Alias>),
Variant72(u32),
Variant73(alloc::vec::Vec<u32>),
Variant74((Option<u32>, Option<ast::Identifier>)),
Variant72(ast::Int),
Variant73(alloc::vec::Vec<ast::Int>),
Variant74((Option<ast::Int>, Option<ast::Identifier>)),
Variant75(ast::MatchCase),
Variant76(alloc::vec::Vec<ast::MatchCase>),
Variant77(ast::PatternKeyword),
@@ -17596,7 +17596,7 @@ mod __parse__Top {
fn __pop_Variant74<
>(
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>
) -> (TextSize, (Option<u32>, Option<ast::Identifier>), TextSize)
) -> (TextSize, (Option<ast::Int>, Option<ast::Identifier>), TextSize)
{
match __symbols.pop() {
Some((__l, __Symbol::Variant74(__v), __r)) => (__l, __v, __r),
@@ -17973,6 +17973,16 @@ mod __parse__Top {
_ => __symbol_type_mismatch()
}
}
fn __pop_Variant73<
>(
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>
) -> (TextSize, alloc::vec::Vec<ast::Int>, TextSize)
{
match __symbols.pop() {
Some((__l, __Symbol::Variant73(__v), __r)) => (__l, __v, __r),
_ => __symbol_type_mismatch()
}
}
fn __pop_Variant76<
>(
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>
@@ -18043,16 +18053,6 @@ mod __parse__Top {
_ => __symbol_type_mismatch()
}
}
fn __pop_Variant73<
>(
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>
) -> (TextSize, alloc::vec::Vec<u32>, TextSize)
{
match __symbols.pop() {
Some((__l, __Symbol::Variant73(__v), __r)) => (__l, __v, __r),
_ => __symbol_type_mismatch()
}
}
fn __pop_Variant70<
>(
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>
@@ -18143,6 +18143,16 @@ mod __parse__Top {
_ => __symbol_type_mismatch()
}
}
fn __pop_Variant72<
>(
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>
) -> (TextSize, ast::Int, TextSize)
{
match __symbols.pop() {
Some((__l, __Symbol::Variant72(__v), __r)) => (__l, __v, __r),
_ => __symbol_type_mismatch()
}
}
fn __pop_Variant75<
>(
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>
@@ -18513,16 +18523,6 @@ mod __parse__Top {
_ => __symbol_type_mismatch()
}
}
fn __pop_Variant72<
>(
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>
) -> (TextSize, u32, TextSize)
{
match __symbols.pop() {
Some((__l, __Symbol::Variant72(__v), __r)) => (__l, __v, __r),
_ => __symbol_type_mismatch()
}
}
pub(crate) fn __reduce0<
>(
mode: Mode,
@@ -31464,7 +31464,7 @@ fn __action61<
mode: Mode,
(_, location, _): (TextSize, TextSize, TextSize),
(_, _, _): (TextSize, token::Tok, TextSize),
(_, source, _): (TextSize, (Option<u32>, Option<ast::Identifier>), TextSize),
(_, source, _): (TextSize, (Option<ast::Int>, Option<ast::Identifier>), TextSize),
(_, _, _): (TextSize, token::Tok, TextSize),
(_, names, _): (TextSize, Vec<ast::Alias>, TextSize),
(_, end_location, _): (TextSize, TextSize, TextSize),
@@ -31488,12 +31488,12 @@ fn __action61<
fn __action62<
>(
mode: Mode,
(_, dots, _): (TextSize, alloc::vec::Vec<u32>, TextSize),
(_, dots, _): (TextSize, alloc::vec::Vec<ast::Int>, TextSize),
(_, name, _): (TextSize, ast::Identifier, TextSize),
) -> (Option<u32>, Option<ast::Identifier>)
) -> (Option<ast::Int>, Option<ast::Identifier>)
{
{
(Some(dots.iter().sum()), Some(name))
(Some(ast::Int::new(dots.iter().map(ast::Int::to_u32).sum())), Some(name))
}
}
@@ -31502,11 +31502,11 @@ fn __action62<
fn __action63<
>(
mode: Mode,
(_, dots, _): (TextSize, alloc::vec::Vec<u32>, TextSize),
) -> (Option<u32>, Option<ast::Identifier>)
(_, dots, _): (TextSize, alloc::vec::Vec<ast::Int>, TextSize),
) -> (Option<ast::Int>, Option<ast::Identifier>)
{
{
(Some(dots.iter().sum()), None)
(Some(ast::Int::new(dots.iter().map(ast::Int::to_u32).sum())), None)
}
}
@@ -31516,9 +31516,9 @@ fn __action64<
>(
mode: Mode,
(_, __0, _): (TextSize, token::Tok, TextSize),
) -> u32
) -> ast::Int
{
3
ast::Int::new(3)
}
#[allow(unused_variables)]
@@ -31527,9 +31527,9 @@ fn __action65<
>(
mode: Mode,
(_, __0, _): (TextSize, token::Tok, TextSize),
) -> u32
) -> ast::Int
{
1
ast::Int::new(1)
}
#[allow(unused_variables)]
@@ -36264,8 +36264,8 @@ fn __action364<
fn __action365<
>(
mode: Mode,
(_, __0, _): (TextSize, u32, TextSize),
) -> alloc::vec::Vec<u32>
(_, __0, _): (TextSize, ast::Int, TextSize),
) -> alloc::vec::Vec<ast::Int>
{
alloc::vec![__0]
}
@@ -36275,9 +36275,9 @@ fn __action365<
fn __action366<
>(
mode: Mode,
(_, v, _): (TextSize, alloc::vec::Vec<u32>, TextSize),
(_, e, _): (TextSize, u32, TextSize),
) -> alloc::vec::Vec<u32>
(_, v, _): (TextSize, alloc::vec::Vec<ast::Int>, TextSize),
(_, e, _): (TextSize, ast::Int, TextSize),
) -> alloc::vec::Vec<ast::Int>
{
{ let mut v = v; v.push(e); v }
}
@@ -36289,7 +36289,7 @@ fn __action367<
mode: Mode,
__lookbehind: &TextSize,
__lookahead: &TextSize,
) -> alloc::vec::Vec<u32>
) -> alloc::vec::Vec<ast::Int>
{
alloc::vec![]
}
@@ -36299,8 +36299,8 @@ fn __action367<
fn __action368<
>(
mode: Mode,
(_, v, _): (TextSize, alloc::vec::Vec<u32>, TextSize),
) -> alloc::vec::Vec<u32>
(_, v, _): (TextSize, alloc::vec::Vec<ast::Int>, TextSize),
) -> alloc::vec::Vec<ast::Int>
{
v
}
@@ -45772,7 +45772,7 @@ fn __action806<
>(
mode: Mode,
__0: (TextSize, token::Tok, TextSize),
__1: (TextSize, (Option<u32>, Option<ast::Identifier>), TextSize),
__1: (TextSize, (Option<ast::Int>, Option<ast::Identifier>), TextSize),
__2: (TextSize, token::Tok, TextSize),
__3: (TextSize, Vec<ast::Alias>, TextSize),
__4: (TextSize, TextSize, TextSize),
@@ -59644,7 +59644,7 @@ fn __action1299<
>(
mode: Mode,
__0: (TextSize, token::Tok, TextSize),
__1: (TextSize, (Option<u32>, Option<ast::Identifier>), TextSize),
__1: (TextSize, (Option<ast::Int>, Option<ast::Identifier>), TextSize),
__2: (TextSize, token::Tok, TextSize),
__3: (TextSize, Vec<ast::Alias>, TextSize),
) -> ast::Stmt
@@ -66376,7 +66376,7 @@ fn __action1542<
>(
mode: Mode,
__0: (TextSize, ast::Identifier, TextSize),
) -> (Option<u32>, Option<ast::Identifier>)
) -> (Option<ast::Int>, Option<ast::Identifier>)
{
let __start0 = __0.0;
let __end0 = __0.0;
@@ -66398,9 +66398,9 @@ fn __action1542<
fn __action1543<
>(
mode: Mode,
__0: (TextSize, alloc::vec::Vec<u32>, TextSize),
__0: (TextSize, alloc::vec::Vec<ast::Int>, TextSize),
__1: (TextSize, ast::Identifier, TextSize),
) -> (Option<u32>, Option<ast::Identifier>)
) -> (Option<ast::Int>, Option<ast::Identifier>)
{
let __start0 = __0.0;
let __end0 = __0.2;

View File

@@ -1,6 +1,6 @@
//! This file is generated by `scripts/generate_known_standard_library.py`
pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {
pub fn is_known_standard_library(minor_version: u32, module: &str) -> bool {
matches!(
(minor_version, module),
(

View File

@@ -303,7 +303,7 @@ impl<'a> ParsedModule<'a> {
// TODO(konstin): Add an options for py/pyi to the UI (2/2)
let options = settings
.formatter
.to_format_options(PySourceType::default(), self.source_code);
.to_format_options(PySourceType::default());
format_node(
&self.module,

View File

@@ -15,9 +15,7 @@ license = { workspace = true }
[dependencies]
ruff_linter = { path = "../ruff_linter" }
ruff_formatter = { path = "../ruff_formatter" }
ruff_python_formatter = { path = "../ruff_python_formatter", features = ["serde"] }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_source_file = { path = "../ruff_source_file" }
ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_cache = { path = "../ruff_cache" }
ruff_macros = { path = "../ruff_macros" }
@@ -45,6 +43,4 @@ tempfile = "3.6.0"
[features]
schemars = [ "dep:schemars", "ruff_formatter/schemars", "ruff_python_formatter/schemars" ]
default = []
schemars = [ "dep:schemars" ]

View File

@@ -32,20 +32,17 @@ use ruff_linter::settings::{
use ruff_linter::{
fs, warn_user, warn_user_once, warn_user_once_by_id, RuleSelector, RUFF_PKG_VERSION,
};
use ruff_python_formatter::{MagicTrailingComma, QuoteStyle};
use ruff_python_formatter::{FormatterSettings, MagicTrailingComma, QuoteStyle};
use crate::options::{
Flake8AnnotationsOptions, Flake8BanditOptions, Flake8BugbearOptions, Flake8BuiltinsOptions,
Flake8ComprehensionsOptions, Flake8CopyrightOptions, Flake8ErrMsgOptions, Flake8GetTextOptions,
Flake8ImplicitStrConcatOptions, Flake8ImportConventionsOptions, Flake8PytestStyleOptions,
Flake8QuotesOptions, Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions,
Flake8UnusedArgumentsOptions, FormatOptions, FormatOrOutputFormat, IsortOptions, McCabeOptions,
Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions, PydocstyleOptions,
PyflakesOptions, PylintOptions,
};
use crate::settings::{
FileResolverSettings, FormatterSettings, LineEnding, Settings, EXCLUDE, INCLUDE,
Flake8UnusedArgumentsOptions, IsortOptions, McCabeOptions, Options, Pep8NamingOptions,
PyUpgradeOptions, PycodestyleOptions, PydocstyleOptions, PyflakesOptions, PylintOptions,
};
use crate::settings::{FileResolverSettings, Settings, EXCLUDE, INCLUDE};
#[derive(Debug, Default)]
pub struct RuleSelection {
@@ -116,8 +113,6 @@ pub struct Configuration {
pub pyflakes: Option<PyflakesOptions>,
pub pylint: Option<PylintOptions>,
pub pyupgrade: Option<PyUpgradeOptions>,
pub format: FormatConfiguration,
}
impl Configuration {
@@ -134,28 +129,6 @@ impl Configuration {
let target_version = self.target_version.unwrap_or_default();
let rules = self.as_rule_table();
let preview = self.preview.unwrap_or_default();
let format = self.format;
let format_defaults = FormatterSettings::default();
// TODO(micha): Support changing the tab-width but disallow changing the number of spaces
let formatter = FormatterSettings {
preview: match format.preview.unwrap_or(preview) {
PreviewMode::Disabled => ruff_python_formatter::PreviewMode::Disabled,
PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled,
},
line_width: self
.line_length
.map_or(format_defaults.line_width, |length| {
LineWidth::from(NonZeroU16::from(length))
}),
line_ending: format.line_ending.unwrap_or(format_defaults.line_ending),
indent_style: format.indent_style.unwrap_or(format_defaults.indent_style),
quote_style: format.quote_style.unwrap_or(format_defaults.quote_style),
magic_trailing_comma: format
.magic_trailing_comma
.unwrap_or(format_defaults.magic_trailing_comma),
};
Ok(Settings {
cache_dir: self
@@ -212,7 +185,7 @@ impl Configuration {
.task_tags
.unwrap_or_else(|| TASK_TAGS.iter().map(ToString::to_string).collect()),
logger_objects: self.logger_objects.unwrap_or_default(),
preview,
preview: self.preview.unwrap_or_default(),
typing_modules: self.typing_modules.unwrap_or_default(),
// Plugins
flake8_annotations: self
@@ -317,7 +290,20 @@ impl Configuration {
.unwrap_or_default(),
},
formatter,
formatter: FormatterSettings {
exclude: vec![],
preview: self
.preview
.map(|preview| match preview {
PreviewMode::Disabled => ruff_python_formatter::PreviewMode::Disabled,
PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled,
})
.unwrap_or_default(),
line_width: LineWidth::from(NonZeroU16::from(self.line_length.unwrap_or_default())),
indent_style: IndentStyle::default(),
quote_style: QuoteStyle::default(),
magic_trailing_comma: MagicTrailingComma::default(),
},
})
}
@@ -411,12 +397,7 @@ impl Configuration {
external: options.external,
fix: options.fix,
fix_only: options.fix_only,
output_format: options.output_format.or_else(|| {
options
.format
.as_ref()
.and_then(FormatOrOutputFormat::as_output_format)
}),
output_format: options.output_format.or(options.format),
force_exclude: options.force_exclude,
ignore_init_module_imports: options.ignore_init_module_imports,
include: options.include.map(|paths| {
@@ -480,12 +461,6 @@ impl Configuration {
pyflakes: options.pyflakes,
pylint: options.pylint,
pyupgrade: options.pyupgrade,
format: if let Some(FormatOrOutputFormat::Format(format)) = options.format {
FormatConfiguration::from_options(format)?
} else {
FormatConfiguration::default()
},
})
}
@@ -809,52 +784,6 @@ impl Configuration {
pyflakes: self.pyflakes.combine(config.pyflakes),
pylint: self.pylint.combine(config.pylint),
pyupgrade: self.pyupgrade.combine(config.pyupgrade),
format: self.format.combine(config.format),
}
}
}
#[derive(Debug, Default)]
pub struct FormatConfiguration {
pub preview: Option<PreviewMode>,
pub indent_style: Option<IndentStyle>,
pub quote_style: Option<QuoteStyle>,
pub magic_trailing_comma: Option<MagicTrailingComma>,
pub line_ending: Option<LineEnding>,
}
impl FormatConfiguration {
#[allow(clippy::needless_pass_by_value)]
pub fn from_options(options: FormatOptions) -> Result<Self> {
Ok(Self {
preview: options.preview.map(PreviewMode::from),
indent_style: options.indent_style,
quote_style: options.quote_style,
magic_trailing_comma: options.skip_magic_trailing_comma.map(|skip| {
if skip {
MagicTrailingComma::Ignore
} else {
MagicTrailingComma::Respect
}
}),
line_ending: options.line_ending,
})
}
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn combine(self, other: Self) -> Self {
Self {
preview: self.preview.or(other.preview),
indent_style: self.indent_style.or(other.indent_style),
quote_style: self.quote_style.or(other.quote_style),
magic_trailing_comma: self.magic_trailing_comma.or(other.magic_trailing_comma),
line_ending: self.line_ending.or(other.line_ending),
}
}
}

View File

@@ -6,7 +6,7 @@ pub mod resolver;
pub mod options_base;
mod settings;
pub use settings::{FileResolverSettings, FormatterSettings, Settings};
pub use settings::Settings;
#[cfg(test)]
mod tests {

View File

@@ -1,13 +1,4 @@
use std::collections::BTreeSet;
use std::hash::BuildHasherDefault;
use regex::Regex;
use ruff_formatter::IndentStyle;
use rustc_hash::{FxHashMap, FxHashSet};
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use crate::options_base::{OptionsMetadata, Visit};
use ruff_linter::line_width::{LineLength, TabSize};
use ruff_linter::rules::flake8_pytest_style::settings::SettingsError;
use ruff_linter::rules::flake8_pytest_style::types;
@@ -28,9 +19,11 @@ use ruff_linter::settings::types::{
};
use ruff_linter::{warn_user_once, RuleSelector};
use ruff_macros::{CombineOptions, ConfigurationOptions};
use ruff_python_formatter::QuoteStyle;
use crate::settings::LineEnding;
use rustc_hash::{FxHashMap, FxHashSet};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::hash::BuildHasherDefault;
use strum::IntoEnumIterator;
#[derive(Debug, PartialEq, Eq, Default, ConfigurationOptions, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
@@ -259,6 +252,17 @@ pub struct Options {
)]
pub fixable: Option<Vec<RuleSelector>>,
/// The style in which violation messages should be formatted: `"text"`
/// (default), `"grouped"` (group messages by file), `"json"`
/// (machine-readable), `"junit"` (machine-readable XML), `"github"` (GitHub
/// Actions annotations), `"gitlab"` (GitLab CI code quality report),
/// `"pylint"` (Pylint text format) or `"azure"` (Azure Pipeline logging commands).
///
/// This option has been **deprecated** in favor of `output-format`
/// to avoid ambiguity with Ruff's upcoming formatter.
#[cfg_attr(feature = "schemars", schemars(skip))]
pub format: Option<SerializationFormat>,
/// The style in which violation messages should be formatted: `"text"`
/// (default), `"grouped"` (group messages by file), `"json"`
/// (machine-readable), `"junit"` (machine-readable XML), `"github"` (GitHub
@@ -677,20 +681,6 @@ pub struct Options {
#[option_group]
pub pyupgrade: Option<PyUpgradeOptions>,
/// Options to configure the code formatting.
///
/// Previously:
/// The style in which violation messages should be formatted: `"text"`
/// (default), `"grouped"` (group messages by file), `"json"`
/// (machine-readable), `"junit"` (machine-readable XML), `"github"` (GitHub
/// Actions annotations), `"gitlab"` (GitLab CI code quality report),
/// `"pylint"` (Pylint text format) or `"azure"` (Azure Pipeline logging commands).
///
/// This option has been **deprecated** in favor of `output-format`
/// to avoid ambiguity with Ruff's upcoming formatter.
#[option_group]
pub format: Option<FormatOrOutputFormat>,
// Tables are required to go last.
/// A list of mappings from file pattern to rule codes or prefixes to
/// exclude, when considering any matching files.
@@ -2391,138 +2381,10 @@ impl PyUpgradeOptions {
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum FormatOrOutputFormat {
Format(FormatOptions),
OutputFormat(SerializationFormat),
}
impl FormatOrOutputFormat {
pub const fn as_output_format(&self) -> Option<SerializationFormat> {
match self {
FormatOrOutputFormat::Format(_) => None,
FormatOrOutputFormat::OutputFormat(format) => Some(*format),
}
}
}
impl OptionsMetadata for FormatOrOutputFormat {
fn record(visit: &mut dyn Visit) {
FormatOptions::record(visit);
}
fn documentation() -> Option<&'static str> {
FormatOptions::documentation()
}
}
/// Experimental: Configures how `ruff format` formats your code.
///
/// Please provide feedback in [this discussion](https://github.com/astral-sh/ruff/discussions/7310).
#[derive(
Debug, PartialEq, Eq, Default, Serialize, Deserialize, ConfigurationOptions, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FormatOptions {
/// Whether to enable the unstable preview style formatting.
#[option(
default = "false",
value_type = "bool",
example = r#"
# Enable preview style formatting
preview = true
"#
)]
pub preview: Option<bool>,
/// Whether to use 4 spaces or hard tabs for indenting code.
///
/// Defaults to 4 spaces. We care about accessibility; if you do not need tabs for accessibility, we do not recommend you use them.
#[option(
default = "space",
value_type = r#""space" | "tab""#,
example = r#"
# Use tabs instead of 4 space indentation
indent-style = "tab"
"#
)]
pub indent_style: Option<IndentStyle>,
/// Whether to prefer single `'` or double `"` quotes for strings and docstrings.
///
/// Ruff may deviate from this option if using the configured quotes would require more escaped quotes:
///
/// ```python
/// a = "It's monday morning"
/// b = "a string without any quotes"
/// ```
///
/// Ruff leaves `a` unchanged when using `quote-style = "single"` because it is otherwise
/// necessary to escape the `'` which leads to less readable code: `'It\'s monday morning'`.
/// Ruff changes the quotes of `b` to use single quotes.
#[option(
default = r#"double"#,
value_type = r#""double" | "single""#,
example = r#"
# Prefer single quotes over double quotes
quote-style = "single"
"#
)]
pub quote_style: Option<QuoteStyle>,
/// Ruff uses existing trailing commas as an indication that short lines should be left separate.
/// If this option is set to `true`, the magic trailing comma is ignored.
///
/// For example, Ruff leaves the arguments separate even though
/// collapsing the arguments to a single line doesn't exceed the line width if `skip-magic-trailing-comma = false`:
///
/// ```python
/// # The arguments remain on separate lines because of the trailing comma after `b`
/// def test(
/// a,
/// b,
/// ): pass
/// ```
///
/// Setting `skip-magic-trailing-comma = true` changes the formatting to:
///
/// ```python
/// # The arguments remain on separate lines because of the trailing comma after `b`
/// def test(a, b):
/// pass
/// ```
#[option(
default = r#"false"#,
value_type = r#"bool"#,
example = "skip-magic-trailing-comma = true"
)]
pub skip_magic_trailing_comma: Option<bool>,
/// The character Ruff uses at the end of a line.
///
/// * `lf`: Line endings will be converted to `\n`. The default line ending on Unix.
/// * `cr-lf`: Line endings will be converted to `\r\n`. The default line ending on Windows.
/// * `auto`: The newline style is detected automatically on a file per file basis. Files with mixed line endings will be converted to the first detected line ending. Defaults to `\n` for files that contain no line endings.
/// * `native`: Line endings will be converted to `\n` on Unix and `\r\n` on Windows.
#[option(
default = r#"lf"#,
value_type = r#""lf" | "crlf" | "auto" | "native""#,
example = r#"
# Automatically detect the line ending on a file per file basis.
quote-style = "auto"
"#
)]
pub line_ending: Option<LineEnding>,
}
#[cfg(test)]
mod tests {
use ruff_linter::rules::flake8_self;
use crate::options::Flake8SelfOptions;
use ruff_linter::rules::flake8_self;
#[test]
fn flake8_self_options() {

View File

@@ -1,308 +1,167 @@
use std::fmt::{Debug, Display, Formatter};
use std::fmt::{Display, Formatter};
/// Visits [`OptionsMetadata`].
///
/// An instance of [`Visit`] represents the logic for inspecting an object's options metadata.
pub trait Visit {
/// Visits an [`OptionField`] value named `name`.
fn record_field(&mut self, name: &str, field: OptionField);
/// Visits an [`OptionSet`] value named `name`.
fn record_set(&mut self, name: &str, group: OptionSet);
}
/// Returns metadata for its options.
pub trait OptionsMetadata {
/// Visits the options metadata of this object by calling `visit` for each option.
fn record(visit: &mut dyn Visit);
fn documentation() -> Option<&'static str> {
None
}
/// Returns the extracted metadata.
fn metadata() -> OptionSet
where
Self: Sized + 'static,
{
OptionSet::of::<Self>()
}
}
/// Metadata of an option that can either be a [`OptionField`] or [`OptionSet`].
#[derive(Clone, PartialEq, Eq, Debug)]
#[derive(Debug, Eq, PartialEq)]
pub enum OptionEntry {
/// A single option.
Field(OptionField),
/// A set of options
Set(OptionSet),
Group(OptionGroup),
}
impl Display for OptionEntry {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
OptionEntry::Set(set) => std::fmt::Display::fmt(set, f),
OptionEntry::Field(field) => std::fmt::Display::fmt(&field, f),
OptionEntry::Field(field) => field.fmt(f),
OptionEntry::Group(group) => group.fmt(f),
}
}
}
/// A set of options.
///
/// It extracts the options by calling the [`OptionsMetadata::record`] of a type implementing
/// [`OptionsMetadata`].
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct OptionSet {
record: fn(&mut dyn Visit),
doc: fn() -> Option<&'static str>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct OptionGroup(&'static [(&'static str, OptionEntry)]);
impl OptionSet {
pub fn of<T>() -> Self
where
T: OptionsMetadata + 'static,
{
Self {
record: T::record,
doc: T::documentation,
}
impl OptionGroup {
pub const fn new(options: &'static [(&'static str, OptionEntry)]) -> Self {
Self(options)
}
/// Visits the options in this set by calling `visit` for each option.
pub fn record(&self, visit: &mut dyn Visit) {
let record = self.record;
record(visit);
pub fn iter(&self) -> std::slice::Iter<(&str, OptionEntry)> {
self.into_iter()
}
pub fn documentation(&self) -> Option<&'static str> {
let documentation = self.doc;
documentation()
}
/// Returns `true` if this set has an option that resolves to `name`.
///
/// The name can be separated by `.` to find a nested option.
/// Get an option entry by its fully-qualified name
/// (e.g. `foo.bar` refers to the `bar` option in the `foo` group).
///
/// ## Examples
///
/// ### Test for the existence of a child option
/// ### Find a direct child
///
/// ```rust
/// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit};
/// # use ruff_workspace::options_base::{OptionGroup, OptionEntry, OptionField};
///
/// struct WithOptions;
/// const options: [(&'static str, OptionEntry); 2] = [
/// ("ignore_names", OptionEntry::Field(OptionField {
/// doc: "ignore_doc",
/// default: "ignore_default",
/// value_type: "value_type",
/// example: "ignore code"
/// })),
///
/// impl OptionsMetadata for WithOptions {
/// fn record(visit: &mut dyn Visit) {
/// visit.record_field("ignore-git-ignore", OptionField {
/// doc: "Whether Ruff should respect the gitignore file",
/// default: "false",
/// value_type: "bool",
/// example: "",
/// });
/// ("global_names", OptionEntry::Field(OptionField {
/// doc: "global_doc",
/// default: "global_default",
/// value_type: "value_type",
/// example: "global code"
/// }))
/// ];
///
/// let group = OptionGroup::new(&options);
///
/// let ignore_names = group.get("ignore_names");
///
/// match ignore_names {
/// None => panic!("Expect option 'ignore_names' to be Some"),
/// Some(OptionEntry::Group(group)) => panic!("Expected 'ignore_names' to be a field but found group {}", group),
/// Some(OptionEntry::Field(field)) => {
/// assert_eq!("ignore_doc", field.doc);
/// }
/// }
///
/// assert!(WithOptions::metadata().has("ignore-git-ignore"));
/// assert!(!WithOptions::metadata().has("does-not-exist"));
/// assert_eq!(None, group.get("not_existing_option"));
/// ```
/// ### Test for the existence of a nested option
///
/// ### Find a nested options
///
/// ```rust
/// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit};
/// # use ruff_workspace::options_base::{OptionGroup, OptionEntry, OptionField};
///
/// struct Root;
/// const ignore_options: [(&'static str, OptionEntry); 2] = [
/// ("names", OptionEntry::Field(OptionField {
/// doc: "ignore_name_doc",
/// default: "ignore_name_default",
/// value_type: "value_type",
/// example: "ignore name code"
/// })),
///
/// impl OptionsMetadata for Root {
/// fn record(visit: &mut dyn Visit) {
/// visit.record_field("ignore-git-ignore", OptionField {
/// doc: "Whether Ruff should respect the gitignore file",
/// default: "false",
/// value_type: "bool",
/// example: "",
/// });
/// ("extensions", OptionEntry::Field(OptionField {
/// doc: "ignore_extensions_doc",
/// default: "ignore_extensions_default",
/// value_type: "value_type",
/// example: "ignore extensions code"
/// }))
/// ];
///
/// visit.record_set("format", Nested::metadata());
/// const options: [(&'static str, OptionEntry); 2] = [
/// ("ignore", OptionEntry::Group(OptionGroup::new(&ignore_options))),
///
/// ("global_names", OptionEntry::Field(OptionField {
/// doc: "global_doc",
/// default: "global_default",
/// value_type: "value_type",
/// example: "global code"
/// }))
/// ];
///
/// let group = OptionGroup::new(&options);
///
/// let ignore_names = group.get("ignore.names");
///
/// match ignore_names {
/// None => panic!("Expect option 'ignore.names' to be Some"),
/// Some(OptionEntry::Group(group)) => panic!("Expected 'ignore_names' to be a field but found group {}", group),
/// Some(OptionEntry::Field(field)) => {
/// assert_eq!("ignore_name_doc", field.doc);
/// }
/// }
///
/// struct Nested;
///
/// impl OptionsMetadata for Nested {
/// fn record(visit: &mut dyn Visit) {
/// visit.record_field("hard-tabs", OptionField {
/// doc: "Use hard tabs for indentation and spaces for alignment.",
/// default: "false",
/// value_type: "bool",
/// example: "",
/// });
/// }
/// }
///
/// assert!(Root::metadata().has("format.hard-tabs"));
/// assert!(!Root::metadata().has("format.spaces"));
/// assert!(!Root::metadata().has("lint.hard-tabs"));
/// ```
pub fn has(&self, name: &str) -> bool {
self.find(name).is_some()
}
pub fn get(&self, name: &str) -> Option<&OptionEntry> {
let mut parts = name.split('.').peekable();
/// Returns `Some` if this set has an option that resolves to `name` and `None` otherwise.
///
/// The name can be separated by `.` to find a nested option.
///
/// ## Examples
///
/// ### Find a child option
///
/// ```rust
/// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit};
///
/// struct WithOptions;
///
/// static IGNORE_GIT_IGNORE: OptionField = OptionField {
/// doc: "Whether Ruff should respect the gitignore file",
/// default: "false",
/// value_type: "bool",
/// example: "",
/// };
///
/// impl OptionsMetadata for WithOptions {
/// fn record(visit: &mut dyn Visit) {
/// visit.record_field("ignore-git-ignore", IGNORE_GIT_IGNORE.clone());
/// }
/// }
///
/// assert_eq!(WithOptions::metadata().find("ignore-git-ignore"), Some(OptionEntry::Field(IGNORE_GIT_IGNORE.clone())));
/// assert_eq!(WithOptions::metadata().find("does-not-exist"), None);
/// ```
/// ### Find a nested option
///
/// ```rust
/// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit};
///
/// static HARD_TABS: OptionField = OptionField {
/// doc: "Use hard tabs for indentation and spaces for alignment.",
/// default: "false",
/// value_type: "bool",
/// example: "",
/// };
///
/// struct Root;
///
/// impl OptionsMetadata for Root {
/// fn record(visit: &mut dyn Visit) {
/// visit.record_field("ignore-git-ignore", OptionField {
/// doc: "Whether Ruff should respect the gitignore file",
/// default: "false",
/// value_type: "bool",
/// example: "",
/// });
///
/// visit.record_set("format", Nested::metadata());
/// }
/// }
///
/// struct Nested;
///
/// impl OptionsMetadata for Nested {
/// fn record(visit: &mut dyn Visit) {
/// visit.record_field("hard-tabs", HARD_TABS.clone());
/// }
/// }
///
/// assert_eq!(Root::metadata().find("format.hard-tabs"), Some(OptionEntry::Field(HARD_TABS.clone())));
/// assert_eq!(Root::metadata().find("format"), Some(OptionEntry::Set(Nested::metadata())));
/// assert_eq!(Root::metadata().find("format.spaces"), None);
/// assert_eq!(Root::metadata().find("lint.hard-tabs"), None);
/// ```
pub fn find(&self, name: &str) -> Option<OptionEntry> {
struct FindOptionVisitor<'a> {
option: Option<OptionEntry>,
parts: std::str::Split<'a, char>,
needle: &'a str,
}
let mut options = self.iter();
impl Visit for FindOptionVisitor<'_> {
fn record_set(&mut self, name: &str, set: OptionSet) {
if self.option.is_none() && name == self.needle {
if let Some(next) = self.parts.next() {
self.needle = next;
set.record(self);
} else {
self.option = Some(OptionEntry::Set(set));
}
}
}
loop {
let part = parts.next()?;
fn record_field(&mut self, name: &str, field: OptionField) {
if self.option.is_none() && name == self.needle {
if self.parts.next().is_none() {
self.option = Some(OptionEntry::Field(field));
}
let (_, field) = options.find(|(name, _)| *name == part)?;
match (parts.peek(), field) {
(None, field) => return Some(field),
(Some(..), OptionEntry::Field(..)) => return None,
(Some(..), OptionEntry::Group(group)) => {
options = group.iter();
}
}
}
}
}
let mut parts = name.split('.');
impl<'a> IntoIterator for &'a OptionGroup {
type IntoIter = std::slice::Iter<'a, (&'a str, OptionEntry)>;
type Item = &'a (&'a str, OptionEntry);
if let Some(first) = parts.next() {
let mut visitor = FindOptionVisitor {
parts,
needle: first,
option: None,
};
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
self.record(&mut visitor);
visitor.option
} else {
None
impl IntoIterator for OptionGroup {
type IntoIter = std::slice::Iter<'static, (&'static str, OptionEntry)>;
type Item = &'static (&'static str, OptionEntry);
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl Display for OptionGroup {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for (name, _) in self {
writeln!(f, "{name}")?;
}
Ok(())
}
}
/// Visitor that writes out the names of all fields and sets.
struct DisplayVisitor<'fmt, 'buf> {
f: &'fmt mut Formatter<'buf>,
result: std::fmt::Result,
}
impl<'fmt, 'buf> DisplayVisitor<'fmt, 'buf> {
fn new(f: &'fmt mut Formatter<'buf>) -> Self {
Self { f, result: Ok(()) }
}
fn finish(self) -> std::fmt::Result {
self.result
}
}
impl Visit for DisplayVisitor<'_, '_> {
fn record_set(&mut self, name: &str, _: OptionSet) {
self.result = self.result.and_then(|_| writeln!(self.f, "{name}"));
}
fn record_field(&mut self, name: &str, _: OptionField) {
self.result = self.result.and_then(|_| writeln!(self.f, "{name}"));
}
}
impl Display for OptionSet {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut visitor = DisplayVisitor::new(f);
self.record(&mut visitor);
visitor.finish()
}
}
impl Debug for OptionSet {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(self, f)
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
#[derive(Debug, Eq, PartialEq)]
pub struct OptionField {
pub doc: &'static str,
pub default: &'static str,

View File

@@ -17,7 +17,6 @@ use ruff_linter::packaging::is_package;
use ruff_linter::{fs, warn_user_once};
use crate::configuration::Configuration;
use crate::options::FormatOrOutputFormat;
use crate::pyproject;
use crate::pyproject::settings_toml;
use crate::settings::Settings;
@@ -221,8 +220,8 @@ fn resolve_configuration(
let options = pyproject::load_options(&path)
.map_err(|err| anyhow!("Failed to parse `{}`: {}", path.display(), err))?;
if matches!(options.format, Some(FormatOrOutputFormat::OutputFormat(_))) {
warn_user_once!("The option `format` has been deprecated to avoid ambiguity with Ruff's upcoming formatter. Use `output-format` instead.");
if options.format.is_some() {
warn_user_once!("The option `format` has been deprecated to avoid ambiguity with Ruff's upcoming formatter. Use `format-output` instead.");
}
let project_root = relativity.resolve(&path);

View File

@@ -1,12 +1,9 @@
use path_absolutize::path_dedot;
use ruff_cache::cache_dir;
use ruff_formatter::{FormatOptions, IndentStyle, LineWidth};
use ruff_linter::settings::types::{FilePattern, FilePatternSet, SerializationFormat};
use ruff_linter::settings::LinterSettings;
use ruff_macros::CacheKey;
use ruff_python_ast::PySourceType;
use ruff_python_formatter::{MagicTrailingComma, PreviewMode, PyFormatOptions, QuoteStyle};
use ruff_source_file::find_newline;
use ruff_python_formatter::FormatterSettings;
use std::path::{Path, PathBuf};
#[derive(Debug, CacheKey)]
@@ -105,88 +102,3 @@ impl FileResolverSettings {
}
}
}
#[derive(CacheKey, Clone, Debug)]
pub struct FormatterSettings {
pub preview: PreviewMode,
pub line_width: LineWidth,
pub indent_style: IndentStyle,
pub quote_style: QuoteStyle,
pub magic_trailing_comma: MagicTrailingComma,
pub line_ending: LineEnding,
}
impl FormatterSettings {
pub fn to_format_options(&self, source_type: PySourceType, source: &str) -> PyFormatOptions {
let line_ending = match self.line_ending {
LineEnding::Lf => ruff_formatter::printer::LineEnding::LineFeed,
LineEnding::CrLf => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed,
#[cfg(target_os = "windows")]
LineEnding::Native => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed,
#[cfg(not(target_os = "windows"))]
LineEnding::Native => ruff_formatter::printer::LineEnding::LineFeed,
LineEnding::Auto => match find_newline(source) {
Some((_, ruff_source_file::LineEnding::Lf)) => {
ruff_formatter::printer::LineEnding::LineFeed
}
Some((_, ruff_source_file::LineEnding::CrLf)) => {
ruff_formatter::printer::LineEnding::CarriageReturnLineFeed
}
Some((_, ruff_source_file::LineEnding::Cr)) => {
ruff_formatter::printer::LineEnding::CarriageReturn
}
None => ruff_formatter::printer::LineEnding::LineFeed,
},
};
PyFormatOptions::from_source_type(source_type)
.with_indent_style(self.indent_style)
.with_quote_style(self.quote_style)
.with_magic_trailing_comma(self.magic_trailing_comma)
.with_preview(self.preview)
.with_line_ending(line_ending)
.with_line_width(self.line_width)
}
}
impl Default for FormatterSettings {
fn default() -> Self {
let default_options = PyFormatOptions::default();
Self {
preview: ruff_python_formatter::PreviewMode::Disabled,
line_width: default_options.line_width(),
line_ending: LineEnding::Lf,
indent_style: default_options.indent_style(),
quote_style: default_options.quote_style(),
magic_trailing_comma: default_options.magic_trailing_comma(),
}
}
}
#[derive(
Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum LineEnding {
/// Line endings will be converted to `\n` as is common on Unix.
#[default]
Lf,
/// Line endings will be converted to `\r\n` as is common on Windows.
CrLf,
/// The newline style is detected automatically on a file per file basis.
/// Files with mixed line endings will be converted to the first detected line ending.
/// Defaults to [`LineEnding::Lf`] for a files that contain no line endings.
Auto,
/// Line endings will be converted to `\n` on Unix and `\r\n` on Windows.
Native,
}

14
docs/.gitignore vendored
View File

@@ -1,5 +1,9 @@
contributing.md
index.md
rules.md
rules/
settings.md
*
!assets
!configuration.md
!editor-integrations.md
!faq.md
!installation.md
!requirements.txt
!tutorial.md
!usage.md

View File

@@ -105,7 +105,7 @@ src = ["src"]
### Rule Selection
Ruff supports [over 700 lint rules](rules.md) split across over 50 built-in plugins, but
Ruff supports [over 600 lint rules](rules.md) split across over 40 built-in plugins, but
determining the right set of rules will depend on your project's needs: some rules may be too
strict, some are framework-specific, and so on.
@@ -247,7 +247,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.291
rev: v0.0.290
hooks:
- id: ruff
```

View File

@@ -23,7 +23,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.291
rev: v0.0.290
hooks:
- id: ruff
```
@@ -33,7 +33,7 @@ Or, to enable autofix:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.291
rev: v0.0.290
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
@@ -44,7 +44,7 @@ Or, to run the hook on Jupyter Notebooks too:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.291
rev: v0.0.290
hooks:
- id: ruff
types_or: [python, pyi, jupyter]

View File

@@ -5,7 +5,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.291"
version = "0.0.290"
description = "An extremely fast Python linter, written in Rust."
authors = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }]
maintainers = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }]

132
ruff.schema.json generated
View File

@@ -326,17 +326,6 @@
"null"
]
},
"format": {
"description": "Options to configure the code formatting.\n\nPreviously: The style in which violation messages should be formatted: `\"text\"` (default), `\"grouped\"` (group messages by file), `\"json\"` (machine-readable), `\"junit\"` (machine-readable XML), `\"github\"` (GitHub Actions annotations), `\"gitlab\"` (GitLab CI code quality report), `\"pylint\"` (Pylint text format) or `\"azure\"` (Azure Pipeline logging commands).\n\nThis option has been **deprecated** in favor of `output-format` to avoid ambiguity with Ruff's upcoming formatter.",
"anyOf": [
{
"$ref": "#/definitions/FormatOrOutputFormat"
},
{
"type": "null"
}
]
},
"ignore": {
"description": "A list of rule codes or prefixes to ignore. Prefixes can specify exact rules (like `F841`), entire categories (like `F`), or anything in between.\n\nWhen breaking ties between enabled and disabled rules (via `select` and `ignore`, respectively), more specific prefixes override less specific prefixes.",
"type": [
@@ -1162,70 +1151,6 @@
},
"additionalProperties": false
},
"FormatOptions": {
"description": "Experimental: Configures how `ruff format` formats your code.\n\nPlease provide feedback in [this discussion](https://github.com/astral-sh/ruff/discussions/7310).",
"type": "object",
"properties": {
"indent-style": {
"description": "Whether to use 4 spaces or hard tabs for indenting code.\n\nDefaults to 4 spaces. We care about accessibility; if you do not need tabs for accessibility, we do not recommend you use them.",
"anyOf": [
{
"$ref": "#/definitions/IndentStyle"
},
{
"type": "null"
}
]
},
"line-ending": {
"description": "The character Ruff uses at the end of a line.\n\n* `lf`: Line endings will be converted to `\\n`. The default line ending on Unix. * `cr-lf`: Line endings will be converted to `\\r\\n`. The default line ending on Windows. * `auto`: The newline style is detected automatically on a file per file basis. Files with mixed line endings will be converted to the first detected line ending. Defaults to `\\n` for files that contain no line endings. * `native`: Line endings will be converted to `\\n` on Unix and `\\r\\n` on Windows.",
"anyOf": [
{
"$ref": "#/definitions/LineEnding"
},
{
"type": "null"
}
]
},
"preview": {
"description": "Whether to enable the unstable preview style formatting.",
"type": [
"boolean",
"null"
]
},
"quote-style": {
"description": "Whether to prefer single `'` or double `\"` quotes for strings and docstrings.\n\nRuff may deviate from this option if using the configured quotes would require more escaped quotes:\n\n```python a = \"It's monday morning\" b = \"a string without any quotes\" ```\n\nRuff leaves `a` unchanged when using `quote-style = \"single\"` because it is otherwise necessary to escape the `'` which leads to less readable code: `'It\\'s monday morning'`. Ruff changes the quotes of `b` to use single quotes.",
"anyOf": [
{
"$ref": "#/definitions/QuoteStyle"
},
{
"type": "null"
}
]
},
"skip-magic-trailing-comma": {
"description": "Ruff uses existing trailing commas as an indication that short lines should be left separate. If this option is set to `true`, the magic trailing comma is ignored.\n\nFor example, Ruff leaves the arguments separate even though collapsing the arguments to a single line doesn't exceed the line width if `skip-magic-trailing-comma = false`:\n\n```python # The arguments remain on separate lines because of the trailing comma after `b` def test( a, b, ): pass ```\n\nSetting `skip-magic-trailing-comma = true` changes the formatting to:\n\n```python # The arguments remain on separate lines because of the trailing comma after `b` def test(a, b): pass ```",
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false
},
"FormatOrOutputFormat": {
"anyOf": [
{
"$ref": "#/definitions/FormatOptions"
},
{
"$ref": "#/definitions/SerializationFormat"
}
]
},
"ImportSection": {
"anyOf": [
{
@@ -1246,24 +1171,6 @@
"local-folder"
]
},
"IndentStyle": {
"oneOf": [
{
"description": "Use tabs to indent code.",
"type": "string",
"enum": [
"tab"
]
},
{
"description": "Use [`IndentWidth`] spaces to indent code.",
"type": "string",
"enum": [
"space"
]
}
]
},
"IsortOptions": {
"type": "object",
"properties": {
@@ -1497,38 +1404,6 @@
},
"additionalProperties": false
},
"LineEnding": {
"oneOf": [
{
"description": "Line endings will be converted to `\\n` as is common on Unix.",
"type": "string",
"enum": [
"lf"
]
},
{
"description": "Line endings will be converted to `\\r\\n` as is common on Windows.",
"type": "string",
"enum": [
"cr-lf"
]
},
{
"description": "The newline style is detected automatically on a file per file basis. Files with mixed line endings will be converted to the first detected line ending. Defaults to [`LineEnding::Lf`] for a files that contain no line endings.",
"type": "string",
"enum": [
"auto"
]
},
{
"description": "Line endings will be converted to `\\n` on Unix and `\\r\\n` on Windows.",
"type": "string",
"enum": [
"native"
]
}
]
},
"LineLength": {
"description": "The length of a line of text that is considered too long.\n\nThe allowed range of values is 1..=320",
"type": "integer",
@@ -1798,13 +1673,6 @@
}
]
},
"QuoteStyle": {
"type": "string",
"enum": [
"single",
"double"
]
},
"RelativeImportsOrder": {
"oneOf": [
{