Compare commits

..

6 Commits

Author SHA1 Message Date
konstin
de239ace74 Add test cases 2023-09-22 15:50:34 +02:00
konstin
53b5121f30 . 2023-09-21 16:58:54 +02:00
konstin
be93983e8e Basic nested range formatting 2023-09-21 15:23:17 +02:00
konstin
a1239b8f2d Make basic range formatting work in vs code 2023-09-21 13:38:05 +02:00
konstin
d1b12acb3c Add byte offset indexing 2023-09-21 13:37:52 +02:00
konstin
1aabf59f77 Formatter and parser refactoring
I got confused and refactored a bit
2023-09-21 13:28:59 +02:00
87 changed files with 1205 additions and 2322 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

14
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",
@@ -2051,7 +2051,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.291"
version = "0.0.290"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2187,7 +2187,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.0.291"
version = "0.0.290"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2322,6 +2322,7 @@ dependencies = [
"bitflags 2.4.0",
"clap",
"countme",
"indoc",
"insta",
"itertools 0.11.0",
"memchr",
@@ -2336,7 +2337,6 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"rustc-hash",
"schemars",
"serde",
"serde_json",
"similar",
@@ -2524,9 +2524,7 @@ dependencies = [
"ruff_formatter",
"ruff_linter",
"ruff_macros",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_source_file",
"rustc-hash",
"schemars",
"serde",

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

@@ -4,7 +4,7 @@ use ruff_benchmark::criterion::{
criterion_group, criterion_main, BenchmarkId, Criterion, Throughput,
};
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_python_formatter::{format_node, PyFormatOptions};
use ruff_python_formatter::{format_module_ast, PyFormatOptions};
use ruff_python_index::CommentRangesBuilder;
use ruff_python_parser::lexer::lex;
use ruff_python_parser::{parse_tokens, Mode};
@@ -65,13 +65,14 @@ fn benchmark_formatter(criterion: &mut Criterion) {
let comment_ranges = comment_ranges.finish();
// Parse the AST.
let python_ast = parse_tokens(tokens, Mode::Module, "<filename>")
let module = parse_tokens(tokens, Mode::Module, "<filename>")
.expect("Input to be a valid python program");
b.iter(|| {
let options = PyFormatOptions::from_extension(Path::new(case.name()));
let formatted = format_node(&python_ast, &comment_ranges, case.code(), options)
.expect("Formatting to succeed");
let formatted =
format_module_ast(&module, &comment_ranges, case.code(), options)
.expect("Formatting to succeed");
formatted.print().expect("Printing to succeed")
});

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 }

View File

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

View File

@@ -11,6 +11,7 @@ use ruff_linter::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat,
};
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
use ruff_python_formatter::LspRowColumn;
use ruff_workspace::configuration::{Configuration, RuleSelection};
use ruff_workspace::resolver::ConfigurationTransformer;
@@ -395,6 +396,14 @@ pub struct FormatCommand {
preview: bool,
#[clap(long, overrides_with("preview"), hide = true)]
no_preview: bool,
/// Range formatting start: Zero-indexed row and zero-indexed char-based column separated by
/// colon, e.g. `1:2`
#[clap(long)]
pub start: Option<LspRowColumn>,
/// Range formatting end: Zero-indexed row and zero-indexed char-based column separated by
/// colon, e.g. `3:4`
#[clap(long)]
pub end: Option<LspRowColumn>,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
@@ -516,6 +525,8 @@ impl FormatCommand {
files: self.files,
isolated: self.isolated,
stdin_filename: self.stdin_filename,
start: self.start,
end: self.end,
},
CliOverrides {
line_length: self.line_length,
@@ -572,6 +583,8 @@ pub struct FormatArguments {
pub files: Vec<PathBuf>,
pub isolated: bool,
pub stdin_filename: Option<PathBuf>,
pub start: Option<LspRowColumn>,
pub end: Option<LspRowColumn>,
}
/// CLI settings that function as configuration overrides.

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_source, 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,17 +139,21 @@ 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 formatted = format_module(&unformatted, options)
let options = options.with_line_ending(line_ending);
let formatted = format_module_source(&unformatted, options)
.map_err(|err| FormatCommandError::FormatModule(Some(path.to_path_buf()), err))?;
let formatted = formatted.as_code();

View File

@@ -5,9 +5,10 @@ use anyhow::Result;
use log::warn;
use ruff_python_ast::PySourceType;
use ruff_python_formatter::format_module;
use ruff_python_formatter::{
format_module_source, format_module_source_range, LspRowColumn, 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 +39,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, cli.start, cli.end) {
Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check => {
@@ -59,30 +65,32 @@ 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,
start: Option<LspRowColumn>,
end: Option<LspRowColumn>,
) -> 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))?;
}
let formatted = if start.is_some() || end.is_some() {
let formatted = format_module_source_range(&unformatted, options, start, end)
.map_err(|err| FormatCommandError::FormatModule(path.map(Path::to_path_buf), err))?;
formatted
} else {
let formatted = format_module_source(&unformatted, options)
.map_err(|err| FormatCommandError::FormatModule(path.map(Path::to_path_buf), err))?;
let formatted = formatted.as_code();
formatted.to_string()
};
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,7 +28,7 @@ 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 }

View File

@@ -34,7 +34,7 @@ use ruff_formatter::{FormatError, LineWidth, PrintError};
use ruff_linter::logging::LogLevel;
use ruff_linter::settings::types::{FilePattern, FilePatternSet};
use ruff_python_formatter::{
format_module, FormatModuleError, MagicTrailingComma, PyFormatOptions,
format_module_source, FormatModuleError, MagicTrailingComma, PyFormatOptions,
};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, Resolver};
@@ -549,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);
}
@@ -799,7 +800,7 @@ fn format_dev_file(
let content = fs::read_to_string(input_path)?;
#[cfg(not(debug_assertions))]
let start = Instant::now();
let printed = match format_module(&content, options.clone()) {
let printed = match format_module_source(&content, options.clone()) {
Ok(printed) => printed,
Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => {
return Err(CheckFileError::SyntaxErrorInInput(err));
@@ -826,7 +827,7 @@ fn format_dev_file(
}
if stability_check {
let reformatted = match format_module(formatted, options) {
let reformatted = match format_module_source(formatted, options) {
Ok(reformatted) => reformatted,
Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => {
return Err(CheckFileError::SyntaxErrorInOutput {

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

@@ -1,6 +1,5 @@
use crate::prelude::TagKind;
use crate::GroupId;
use ruff_text_size::TextRange;
use std::error::Error;
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
@@ -12,7 +11,7 @@ pub enum FormatError {
SyntaxError { message: &'static str },
/// In case range formatting failed because the provided range was larger
/// than the formatted syntax tree
RangeError { input: TextRange, tree: TextRange },
RangeError { row: usize, col: usize },
/// In case printing the document failed because it has an invalid structure.
InvalidDocument(InvalidDocumentError),
@@ -32,9 +31,9 @@ impl std::fmt::Display for FormatError {
FormatError::SyntaxError {message} => {
std::write!(fmt, "syntax error: {message}")
},
FormatError::RangeError { input, tree } => std::write!(
FormatError::RangeError { row, col } => std::write!(
fmt,
"formatting range {input:?} is larger than syntax tree {tree:?}"
"formatting range {row}:{col} is not a valid index"
),
FormatError::InvalidDocument(error) => std::write!(fmt, "Invalid document: {error}\n\n This is an internal Rome error. Please report if necessary."),
FormatError::PoorLayout => {

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

@@ -334,7 +334,7 @@ macro_rules! best_fitting {
$crate::BestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+))
}
}}
}
}
#[cfg(test)]
mod tests {

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 }
@@ -44,6 +43,7 @@ insta = { workspace = true, features = ["glob"] }
serde = { workspace = true }
serde_json = { workspace = true }
similar = { workspace = true }
indoc = "2.0.4"
[[test]]
name = "ruff_python_formatter_fixtures"
@@ -53,5 +53,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

@@ -2,17 +2,17 @@
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use anyhow::{format_err, Context, Result};
use clap::{command, Parser, ValueEnum};
use ruff_formatter::SourceCode;
use ruff_python_index::CommentRangesBuilder;
use ruff_python_parser::lexer::lex;
use ruff_python_parser::{parse_tokens, Mode};
use ruff_text_size::Ranged;
use ruff_python_index::tokens_and_ranges;
use ruff_python_parser::{parse_ok_tokens, Mode};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::comments::collect_comments;
use crate::{format_node, PyFormatOptions};
use crate::{format_module_ast, format_module_range, PyFormatOptions};
#[derive(ValueEnum, Clone, Debug)]
pub enum Emit {
@@ -37,38 +37,46 @@ pub struct Cli {
pub print_ir: bool,
#[clap(long)]
pub print_comments: bool,
/// byte offset for range formatting
#[clap(long)]
pub start: Option<u32>,
/// byte offset for range formatting
#[clap(long)]
pub end: Option<u32>,
}
pub fn format_and_debug_print(input: &str, cli: &Cli, source_type: &Path) -> Result<String> {
let mut tokens = Vec::new();
let mut comment_ranges = CommentRangesBuilder::default();
pub fn format_and_debug_print(source: &str, cli: &Cli, source_type: &Path) -> Result<String> {
let (tokens, comment_ranges) = tokens_and_ranges(source)
.map_err(|err| format_err!("Source contains syntax errors {err:?}"))?;
let module =
parse_ok_tokens(tokens, Mode::Module, "<filename>").context("Syntax error in input")?;
let options = PyFormatOptions::from_extension(source_type);
let source_code = SourceCode::new(source);
let locator = Locator::new(source);
for result in lex(input, Mode::Module) {
let (token, range) = match result {
Ok((token, range)) => (token, range),
Err(err) => bail!("Source contains syntax errors {err:?}"),
};
comment_ranges.visit_token(&token, range);
tokens.push(Ok((token, range)));
if cli.start.is_some() || cli.end.is_some() {
let range = TextRange::new(
cli.start.map(TextSize::new).unwrap_or_default(),
cli.end.map(TextSize::new).unwrap_or(source.text_len()),
);
return Ok(format_module_range(
&module,
&comment_ranges,
source,
options,
&locator,
range,
)?);
}
let comment_ranges = comment_ranges.finish();
// Parse the AST.
let python_ast =
parse_tokens(tokens, Mode::Module, "<filename>").context("Syntax error in input")?;
let options = PyFormatOptions::from_extension(source_type);
let formatted = format_node(&python_ast, &comment_ranges, input, options)
let formatted = format_module_ast(&module, &comment_ranges, source, options)
.context("Failed to format node")?;
if cli.print_ir {
println!("{}", formatted.document().display(SourceCode::new(input)));
println!("{}", formatted.document().display(source_code));
}
if cli.print_comments {
// Print preceding, following and enclosing nodes
let source_code = SourceCode::new(input);
let decorated_comments = collect_comments(&python_ast, source_code, &comment_ranges);
let decorated_comments = collect_comments(&module, source_code, &comment_ranges);
if !decorated_comments.is_empty() {
println!("# Comment decoration: Range, Preceding, Following, Enclosing, Comment");
}
@@ -86,13 +94,10 @@ pub fn format_and_debug_print(input: &str, cli: &Cli, source_type: &Path) -> Res
comment.enclosing_node().kind(),
comment.enclosing_node().range()
),
comment.slice().text(SourceCode::new(input)),
comment.slice().text(source_code),
);
}
println!(
"{:#?}",
formatted.context().comments().debug(SourceCode::new(input))
);
println!("{:#?}", formatted.context().comments().debug(source_code));
}
Ok(formatted
.print()

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

@@ -549,9 +549,9 @@ mod tests {
use ruff_formatter::SourceCode;
use ruff_python_ast::Mod;
use ruff_python_index::CommentRangesBuilder;
use ruff_python_parser::lexer::lex;
use ruff_python_parser::{parse_tokens, Mode};
use ruff_python_index::tokens_and_ranges;
use ruff_python_parser::{parse_ok_tokens, Mode};
use ruff_python_trivia::CommentRanges;
use crate::comments::Comments;
@@ -563,19 +563,11 @@ mod tests {
}
impl<'a> CommentsTestCase<'a> {
fn from_code(code: &'a str) -> Self {
let source_code = SourceCode::new(code);
let tokens: Vec<_> = lex(code, Mode::Module).collect();
let mut comment_ranges = CommentRangesBuilder::default();
for (token, range) in tokens.iter().flatten() {
comment_ranges.visit_token(token, *range);
}
let comment_ranges = comment_ranges.finish();
let parsed = parse_tokens(tokens, Mode::Module, "test.py")
fn from_code(source: &'a str) -> Self {
let source_code = SourceCode::new(source);
let (tokens, comment_ranges) =
tokens_and_ranges(source).expect("Expect source to be valid Python");
let parsed = parse_ok_tokens(tokens, Mode::Module, "test.py")
.expect("Expect source to be valid Python");
CommentsTestCase {

View File

@@ -1,22 +1,29 @@
use std::iter;
use std::str::FromStr;
use thiserror::Error;
use tracing::Level;
use tracing::{warn, Level};
use ruff_formatter::prelude::*;
use ruff_formatter::{format, FormatError, Formatted, PrintError, Printed, SourceCode};
use ruff_python_ast::node::AstNode;
use ruff_python_ast::Mod;
use ruff_python_index::CommentRangesBuilder;
use ruff_python_parser::lexer::{lex, LexicalError};
use ruff_python_parser::{parse_tokens, Mode, ParseError};
use ruff_python_trivia::CommentRanges;
use ruff_python_ast::{
Mod, Stmt, StmtClassDef, StmtFor, StmtFunctionDef, StmtIf, StmtWhile, StmtWith,
};
use ruff_python_index::tokens_and_ranges;
use ruff_python_parser::lexer::LexicalError;
use ruff_python_parser::{parse_ok_tokens, Mode, ParseError};
use ruff_python_trivia::{is_python_whitespace, CommentRanges};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::comments::{
dangling_comments, leading_comments, trailing_comments, Comments, SourceComment,
};
pub use crate::context::PyFormatContext;
pub use crate::options::{MagicTrailingComma, PreviewMode, PyFormatOptions, QuoteStyle};
use crate::statement::suite::SuiteKind;
use crate::verbatim::suppressed_node;
pub use settings::FormatterSettings;
pub(crate) mod builders;
pub mod cli;
@@ -29,6 +36,8 @@ mod options;
pub(crate) mod other;
pub(crate) mod pattern;
mod prelude;
mod range_formatting;
mod settings;
pub(crate) mod statement;
pub(crate) mod type_param;
mod verbatim;
@@ -121,61 +130,260 @@ impl From<ParseError> for FormatModuleError {
}
}
#[tracing::instrument(level = Level::TRACE, skip_all)]
pub fn format_module(
contents: &str,
#[tracing::instrument(name = "format", level = Level::TRACE, skip_all)]
pub fn format_module_source(
source: &str,
options: PyFormatOptions,
) -> Result<Printed, FormatModuleError> {
// Tokenize once
let mut tokens = Vec::new();
let mut comment_ranges = CommentRangesBuilder::default();
for result in lex(contents, Mode::Module) {
let (token, range) = result?;
comment_ranges.visit_token(&token, range);
tokens.push(Ok((token, range)));
}
let comment_ranges = comment_ranges.finish();
// Parse the AST.
let python_ast = parse_tokens(tokens, Mode::Module, "<filename>")?;
let formatted = format_node(&python_ast, &comment_ranges, contents, options)?;
let (tokens, comment_ranges) = tokens_and_ranges(source)?;
let module = parse_ok_tokens(tokens, Mode::Module, "<filename>")?;
let formatted = format_module_ast(&module, &comment_ranges, source, options)?;
Ok(formatted.print()?)
}
pub fn format_node<'a>(
root: &'a Mod,
/// Range formatting coordinate: Zero-indexed row and zero-indexed char-based column separated by
/// colon, e.g. `1:2`.
///
/// See [`Locator::convert_row_and_column`] for details on the semantics.
#[derive(Copy, Clone, Debug, Default)]
pub struct LspRowColumn {
row: usize,
col: usize,
}
impl FromStr for LspRowColumn {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((row, col)) = s.split_once(':') else {
return Err("Coordinate is missing a colon, the format is `<row>:<column>`");
};
Ok(LspRowColumn {
row: row.parse().map_err(|_| "row must be a number")?,
col: col.parse().map_err(|_| "col must be a number")?,
})
}
}
#[tracing::instrument(name = "format", level = Level::TRACE, skip_all)]
pub fn format_module_source_range(
source: &str,
options: PyFormatOptions,
start: Option<LspRowColumn>,
end: Option<LspRowColumn>,
) -> Result<String, FormatModuleError> {
let (tokens, comment_ranges) = tokens_and_ranges(source)?;
let module = parse_ok_tokens(tokens, Mode::Module, "<filename>")?;
let locator = Locator::new(source);
let start = if let Some(start) = start {
locator
.convert_row_and_column(start.row, start.col)
.ok_or(FormatError::RangeError {
row: start.row,
col: start.col,
})?
} else {
TextSize::default()
};
let end = if let Some(end) = end {
locator
.convert_row_and_column(end.row, end.col)
.ok_or(FormatError::RangeError {
row: end.row,
col: end.col,
})?
} else {
source.text_len()
};
let formatted = format_module_range(
&module,
&comment_ranges,
source,
options,
&locator,
TextRange::new(start, end),
)?;
Ok(formatted)
}
pub fn format_module_ast<'a>(
module: &'a Mod,
comment_ranges: &'a CommentRanges,
source: &'a str,
options: PyFormatOptions,
) -> FormatResult<Formatted<PyFormatContext<'a>>> {
let comments = Comments::from_ast(root, SourceCode::new(source), comment_ranges);
let source_code = SourceCode::new(source);
let comments = Comments::from_ast(module, source_code, comment_ranges);
let locator = Locator::new(source);
let formatted = format!(
PyFormatContext::new(options, locator.contents(), comments),
[root.format()]
[module.format()]
)?;
formatted
.context()
.comments()
.assert_all_formatted(SourceCode::new(source));
.assert_all_formatted(source_code);
Ok(formatted)
}
/// Public function for generating a printable string of the debug comments.
pub fn pretty_comments(root: &Mod, comment_ranges: &CommentRanges, source: &str) -> String {
let comments = Comments::from_ast(root, SourceCode::new(source), comment_ranges);
/// Is range inside the body of a node, if we consider the whitespace surrounding the suite as part
/// of the body?
///
/// TODO: Handle leading comments on the first statement
fn range_in_body(suite: &[Stmt], range: TextRange, source: &str) -> bool {
let suite_start = suite.first().unwrap().start();
let suite_end = suite.last().unwrap().end();
std::format!(
"{comments:#?}",
comments = comments.debug(SourceCode::new(source))
)
if range.start() < suite_start
// Extend the range include all whitespace prior to the first statement
&& !source[TextRange::new(range.start(), suite_start)]
.chars()
.all(|c| is_python_whitespace(c))
{
return false;
}
if range.end() > suite_end
// Extend the range include all whitespace after to the last statement
&& !source[TextRange::new(suite_end,range.end())]
.chars()
.all(|c| is_python_whitespace(c))
{
return false;
}
true
}
pub fn format_module_range<'a>(
module: &'a Mod,
comment_ranges: &'a CommentRanges,
source: &'a str,
options: PyFormatOptions,
locator: &Locator<'a>,
range: TextRange,
) -> FormatResult<String> {
let comments = Comments::from_ast(&module, SourceCode::new(source), &comment_ranges);
let Mod::Module(module_inner) = &module else {
panic!("That's not a module");
};
// TODO: Move this to LspRowColumn? we first count chars to then discard that anyway
// Consider someone wanted to format `print(i); print(j)`. This wouldn't work indent-wise, so
// we always do whole lines instead which means we can count indentation normally
// ```python
// if True:
// for i in range(10): j=i+1; print(i); print(j)
// ```
let range = TextRange::new(
locator.line_start(range.start()),
locator.line_end(range.end()),
);
// ```
// a = 1; b = 2; c = 3; d = 4; e = 5
// ^ b end ^ d start
// ^^^^^^^^^^^^^^^ range
// ^ range start ^ range end
// ```
// TODO: If it goes beyond the end of the last stmt or before start, do we need to format
// the parent?
let mut parent_body: &[Stmt] = module_inner.body.as_slice();
let mut in_range;
// TODO: Allow partial inclusions, e.g.
// ```python
// not_formatted = 0
// start = 1
// if cond_formatted:
// last_formatted = 2
// not_formatted_anymore = 3
// ```
// prob a slice and an optional trailing arg
let in_range = loop {
let start = parent_body.partition_point(|child| child.end() < range.start());
let end = parent_body.partition_point(|child| child.start() < range.end());
in_range = &parent_body[start..end];
let [single_stmt] = in_range else {
break in_range;
};
match single_stmt {
Stmt::For(StmtFor { body, .. })
| Stmt::While(StmtWhile { body, .. })
| Stmt::With(StmtWith { body, .. })
| Stmt::FunctionDef(StmtFunctionDef { body, .. })
| Stmt::ClassDef(StmtClassDef { body, .. }) => {
// We need to format the header or a trailing comment
// TODO: ignore trivia
if range_in_body(body, range, source) {
break in_range;
} else {
parent_body = &body;
}
}
Stmt::If(StmtIf {
body,
elif_else_clauses,
..
}) => {
let if_all_end = TextRange::new(
range.start(),
elif_else_clauses
.last()
.map(|clause| clause.body.last().unwrap().end())
.unwrap_or(body.last().unwrap().end()),
);
if !range_in_body(body, if_all_end, source) {
break in_range;
} else if let Some(body) = iter::once(body)
.chain(elif_else_clauses.iter().map(|clause| &clause.body))
.find(|body| range_in_body(body, range, source))
{
parent_body = &body;
} else {
break in_range;
}
}
// | Stmt::StmtTry(ast::StmtTry { body, .. })
// | Stmt::ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler { body, .. })
// | Stmt::ElifElseClause(ast::ElifElseClause { body, .. }) => &body,
// match
_ => break in_range,
}
};
let (Some(first), Some(last)) = (in_range.first(), in_range.last()) else {
// TODO: Use tracing again https://github.com/tokio-rs/tracing/issues/2721
// TODO: Forward this to something proper
eprintln!("The formatting range contains no statements");
return Ok(source.to_string());
};
let mut buffer = source[TextRange::up_to(first.start())].to_string();
let formatted: Formatted<PyFormatContext> = format!(
PyFormatContext::new(options.clone(), locator.contents(), comments),
// TODO: Make suite formatting accept slices
[in_range.to_vec().format().with_options(SuiteKind::TopLevel)]
)?;
//println!("{}", formatted.document().display(SourceCode::new(source)));
// TODO: Make the printer use the buffer instead
buffer += formatted.print_with_indent(1)?.as_code();
buffer += &source[TextRange::new(last.end(), source.text_len())];
return Ok(buffer.to_string());
}
/// Public function for generating a printable string of the debug comments.
pub fn pretty_comments(module: &Mod, comment_ranges: &CommentRanges, source: &str) -> String {
let source_code = SourceCode::new(source);
let comments = Comments::from_ast(module, source_code, comment_ranges);
std::format!("{comments:#?}", comments = comments.debug(source_code))
}
#[cfg(test)]
@@ -185,11 +393,11 @@ mod tests {
use anyhow::Result;
use insta::assert_snapshot;
use ruff_python_index::CommentRangesBuilder;
use ruff_python_parser::lexer::lex;
use ruff_python_parser::{parse_tokens, Mode};
use ruff_python_index::tokens_and_ranges;
use crate::{format_module, format_node, PyFormatOptions};
use ruff_python_parser::{parse_ok_tokens, Mode};
use crate::{format_module_ast, format_module_source, PyFormatOptions};
/// Very basic test intentionally kept very similar to the CLI
#[test]
@@ -205,7 +413,7 @@ if True:
pass
# trailing
"#;
let actual = format_module(input, PyFormatOptions::default())?
let actual = format_module_source(input, PyFormatOptions::default())?
.as_code()
.to_string();
assert_eq!(expected, actual);
@@ -216,7 +424,7 @@ if True:
#[ignore]
#[test]
fn quick_test() {
let src = r#"
let source = r#"
def main() -> None:
if True:
some_very_long_variable_name_abcdefghijk = Foo()
@@ -226,23 +434,13 @@ def main() -> None:
]
"#;
// Tokenize once
let mut tokens = Vec::new();
let mut comment_ranges = CommentRangesBuilder::default();
for result in lex(src, Mode::Module) {
let (token, range) = result.unwrap();
comment_ranges.visit_token(&token, range);
tokens.push(Ok((token, range)));
}
let comment_ranges = comment_ranges.finish();
let (tokens, comment_ranges) = tokens_and_ranges(source).unwrap();
// Parse the AST.
let source_path = "code_inline.py";
let python_ast = parse_tokens(tokens, Mode::Module, source_path).unwrap();
let module = parse_ok_tokens(tokens, Mode::Module, source_path).unwrap();
let options = PyFormatOptions::from_extension(Path::new(source_path));
let formatted = format_node(&python_ast, &comment_ranges, src, options).unwrap();
let formatted = format_module_ast(&module, &comment_ranges, source, options).unwrap();
// Uncomment the `dbg` to print the IR.
// Use `dbg_write!(f, []) instead of `write!(f, [])` in your formatting code to print some IR

View File

@@ -25,11 +25,11 @@ fn main() -> Result<()> {
cli.emit
);
}
let input = read_from_stdin()?;
let source = read_from_stdin()?;
// It seems reasonable to give this a dummy name
let formatted = format_and_debug_print(&input, &cli, Path::new("stdin.py"))?;
let formatted = format_and_debug_print(&source, &cli, Path::new("stdin.py"))?;
if cli.check {
if formatted == input {
if formatted == source {
return Ok(());
}
bail!("Content not correctly formatted")
@@ -37,9 +37,9 @@ fn main() -> Result<()> {
stdout().lock().write_all(formatted.as_bytes())?;
} else {
for file in &cli.files {
let input = fs::read_to_string(file)
let source = fs::read_to_string(file)
.with_context(|| format!("Could not read {}: ", file.display()))?;
let formatted = format_and_debug_print(&input, &cli, file)?;
let formatted = format_and_debug_print(&source, &cli, file)?;
match cli.emit {
Some(Emit::Stdout) => stdout().lock().write_all(formatted.as_bytes())?,
None | Some(Emit::Files) => {

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]

View File

@@ -0,0 +1,226 @@
#[cfg(test)]
mod tests {
use crate::{format_module_source_range, LspRowColumn, PyFormatOptions};
use indoc::indoc;
use insta::assert_snapshot;
fn format(source: &str, start: (usize, usize), end: (usize, usize)) -> String {
format_module_source_range(
source,
PyFormatOptions::default(),
Some(LspRowColumn {
row: start.0,
col: start.1,
}),
Some(LspRowColumn {
row: end.0,
col: end.1,
}),
)
.unwrap()
}
#[test]
fn test_top_level() {
assert_snapshot!(format(indoc! {r#"
a = [1,]
b = [1,]
c = [1,]
d = [1,]
"#}, (1, 3), (2, 5)), @r###"
a = [1,]
b = [
1,
]
c = [
1,
]
d = [1,]
"###);
}
#[test]
fn test_easy_nested() {
assert_snapshot!(format(indoc! {r#"
a = [1,]
for i in range( 1 ):
b = [1,]
c = [1,]
d = [1,]
e = [1,]
"#}, (3, 3), (3, 5)), @r###"
a = [1,]
for i in range(1):
b = [
1,
]
c = [
1,
]
d = [
1,
]
e = [1,]
"###);
}
#[test]
fn test_if() {
let source = indoc! {r#"
import random
if random.random() < 0.5:
a = [1,]
b = [1,]
elif random.random() < 0.75:
c = [1,]
d = [1,]
else:
e = [1,]
f = [1,]
g = [1,]
"#};
assert_snapshot!(format(source, (3, 0), (3, 10)), @r###"
import random
if random.random() < 0.5:
a = [
1,
]
b = [
1,
]
elif random.random() < 0.75:
c = [
1,
]
d = [
1,
]
else:
e = [
1,
]
f = [
1,
]
g = [1,]
"###);
assert_snapshot!(format(source, (6, 0), (6, 10)), @r###"
import random
if random.random() < 0.5:
a = [
1,
]
b = [
1,
]
elif random.random() < 0.75:
c = [
1,
]
d = [
1,
]
else:
e = [
1,
]
f = [
1,
]
g = [1,]
"###);
assert_snapshot!(format(source, (9, 0), (9, 10)), @r###"
import random
if random.random() < 0.5:
a = [
1,
]
b = [
1,
]
elif random.random() < 0.75:
c = [
1,
]
d = [
1,
]
else:
e = [
1,
]
f = [
1,
]
g = [1,]
"###);
assert_snapshot!(format(source, (3, 0), (6, 10)), @r###"
import random
if random.random() < 0.5:
a = [
1,
]
b = [
1,
]
elif random.random() < 0.75:
c = [
1,
]
d = [
1,
]
else:
e = [
1,
]
f = [
1,
]
g = [1,]
"###);
}
// TODO
#[test]
fn test_trailing_comment() {
assert_snapshot!(format(indoc! {r#"
if True:
a = [1,]
# trailing comment
"#}, (1, 3), (2, 5)), @r###"
if True:
a = [
1,
]
# trailing comment
"###);
}
// TODO
#[test]
fn test_alternative_indent() {
assert_snapshot!(format(indoc! {r#"
if True:
a = [1,]
b = [1,]
c = [1,]
"#}, (1, 3), (2, 5)), @r###"
if True:
a = [
1,
]
b = [
1,
]
c = [1,]
"###);
}
}

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,5 +1,5 @@
use ruff_formatter::FormatOptions;
use ruff_python_formatter::{format_module, PyFormatOptions};
use ruff_python_formatter::{format_module_source, PyFormatOptions};
use similar::TextDiff;
use std::fmt::{Formatter, Write};
use std::io::BufReader;
@@ -20,7 +20,7 @@ fn black_compatibility() {
PyFormatOptions::from_extension(input_path)
};
let printed = format_module(&content, options.clone()).unwrap_or_else(|err| {
let printed = format_module_source(&content, options.clone()).unwrap_or_else(|err| {
panic!(
"Formatting of {} to succeed but encountered error {err}",
input_path.display()
@@ -107,7 +107,8 @@ fn format() {
let content = fs::read_to_string(input_path).unwrap();
let options = PyFormatOptions::from_extension(input_path);
let printed = format_module(&content, options.clone()).expect("Formatting to succeed");
let printed =
format_module_source(&content, options.clone()).expect("Formatting to succeed");
let formatted_code = printed.as_code();
ensure_stability_when_formatting_twice(formatted_code, options.clone(), input_path);
@@ -124,7 +125,7 @@ fn format() {
for (i, options) in options.into_iter().enumerate() {
let printed =
format_module(&content, options.clone()).expect("Formatting to succeed");
format_module_source(&content, options.clone()).expect("Formatting to succeed");
let formatted_code = printed.as_code();
ensure_stability_when_formatting_twice(formatted_code, options.clone(), input_path);
@@ -139,7 +140,8 @@ fn format() {
.unwrap();
}
} else {
let printed = format_module(&content, options.clone()).expect("Formatting to succeed");
let printed =
format_module_source(&content, options.clone()).expect("Formatting to succeed");
let formatted_code = printed.as_code();
ensure_stability_when_formatting_twice(formatted_code, options, input_path);
@@ -174,7 +176,7 @@ fn ensure_stability_when_formatting_twice(
options: PyFormatOptions,
input_path: &Path,
) {
let reformatted = match format_module(formatted_code, options) {
let reformatted = match format_module_source(formatted_code, options) {
Ok(reformatted) => reformatted,
Err(err) => {
panic!(

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

@@ -1,6 +1,7 @@
use std::fmt::Debug;
use ruff_python_parser::Tok;
use ruff_python_parser::lexer::{lex, LexicalError};
use ruff_python_parser::{Mode, Tok};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::TextRange;
@@ -20,3 +21,21 @@ impl CommentRangesBuilder {
CommentRanges::new(self.ranges)
}
}
/// Helper method to lex and extract comment ranges
pub fn tokens_and_ranges(
source: &str,
) -> Result<(Vec<(Tok, TextRange)>, CommentRanges), LexicalError> {
let mut tokens = Vec::new();
let mut comment_ranges = CommentRangesBuilder::default();
for result in lex(source, Mode::Module) {
let (token, range) = result?;
comment_ranges.visit_token(&token, range);
tokens.push((token, range));
}
let comment_ranges = comment_ranges.finish();
Ok((tokens, comment_ranges))
}

View File

@@ -1,5 +1,5 @@
mod comment_ranges;
mod indexer;
pub use comment_ranges::CommentRangesBuilder;
pub use comment_ranges::{tokens_and_ranges, CommentRangesBuilder};
pub use indexer::Indexer;

View File

@@ -110,8 +110,8 @@
//! [lexer]: crate::lexer
pub use parser::{
parse, parse_expression, parse_expression_starts_at, parse_program, parse_starts_at,
parse_suite, parse_tokens, ParseError, ParseErrorType,
parse, parse_expression, parse_expression_starts_at, parse_ok_tokens, parse_program,
parse_starts_at, parse_suite, parse_tokens, ParseError, ParseErrorType,
};
use ruff_python_ast::{CmpOp, Expr, Mod, PySourceType, Suite};
use ruff_text_size::{Ranged, TextRange, TextSize};

View File

@@ -18,7 +18,7 @@ use itertools::Itertools;
pub(super) use lalrpop_util::ParseError as LalrpopError;
use ruff_text_size::{TextRange, TextSize};
use crate::lexer::{lex, lex_starts_at};
use crate::lexer::{lex, lex_starts_at, Spanned};
use crate::{
lexer::{self, LexResult, LexicalError, LexicalErrorType},
python,
@@ -159,7 +159,7 @@ pub fn parse_expression_starts_at(
/// let program = parse(source, Mode::Ipython, "<embedded>");
/// assert!(program.is_ok());
/// ```
pub fn parse(source: &str, mode: Mode, source_path: &str) -> Result<ast::Mod, ParseError> {
pub fn parse(source: &str, mode: Mode, source_path: &str) -> Result<Mod, ParseError> {
parse_starts_at(source, mode, source_path, TextSize::default())
}
@@ -191,7 +191,7 @@ pub fn parse_starts_at(
mode: Mode,
source_path: &str,
offset: TextSize,
) -> Result<ast::Mod, ParseError> {
) -> Result<Mod, ParseError> {
let lxr = lexer::lex_starts_at(source, mode, offset);
parse_tokens(lxr, mode, source_path)
}
@@ -215,7 +215,7 @@ pub fn parse_tokens(
lxr: impl IntoIterator<Item = LexResult>,
mode: Mode,
source_path: &str,
) -> Result<ast::Mod, ParseError> {
) -> Result<Mod, ParseError> {
let lxr = lxr.into_iter();
parse_filtered_tokens(
@@ -225,19 +225,35 @@ pub fn parse_tokens(
)
}
/// Parse tokens into an AST like [`parse_tokens`], but we already know all tokens are valid.
pub fn parse_ok_tokens(
lxr: impl IntoIterator<Item = Spanned>,
mode: Mode,
source_path: &str,
) -> Result<Mod, ParseError> {
let lxr = lxr
.into_iter()
.filter(|(tok, _)| !matches!(tok, Tok::Comment { .. } | Tok::NonLogicalNewline));
let marker_token = (Tok::start_marker(mode), TextRange::default());
let lexer = iter::once(marker_token)
.chain(lxr)
.map(|(t, range)| (range.start(), t, range.end()));
python::TopParser::new()
.parse(mode, lexer)
.map_err(|e| parse_error_from_lalrpop(e, source_path))
}
fn parse_filtered_tokens(
lxr: impl IntoIterator<Item = LexResult>,
mode: Mode,
source_path: &str,
) -> Result<ast::Mod, ParseError> {
) -> Result<Mod, ParseError> {
let marker_token = (Tok::start_marker(mode), TextRange::default());
let lexer = iter::once(Ok(marker_token)).chain(lxr);
python::TopParser::new()
.parse(
mode,
lexer
.into_iter()
.map_ok(|(t, range)| (range.start(), t, range.end())),
lexer.map_ok(|(t, range)| (range.start(), t, range.end())),
)
.map_err(|e| parse_error_from_lalrpop(e, source_path))
}

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

@@ -441,6 +441,75 @@ impl<'a> Locator<'a> {
}
}
/// Compute the byte offset from zero-indexed row and column indices.
///
/// We get row and column from the LSP. E.g.
/// ```text
/// a=(1,2,)
/// b=(3,4,)
/// ^
/// c=(5,6,)
/// ```
/// has coordinates `1:2`. Note that indices are computed in chars, e.g.
/// ```text
/// a=(1,2,)
/// "안녕"
/// ^
/// ```
/// where the first syllable is a single character (two bytes), we get `1:2`, while for
/// ```text
/// a=(1,2,)
/// "감기"
/// ^
/// ```
/// where the first syllable is three characters (three times two bytes), we get `1:4`.
///
/// ```rust
/// # use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
/// # use ruff_source_file::Locator;
///
/// let source = "a=(1,2,)\nb=(3,4,)";
/// let locator = Locator::new(source);
/// let offset = locator.convert_row_and_column(1, 2).unwrap();
/// assert_eq!(&source[TextRange::new(offset, source.text_len())], "(3,4,)");
///
/// let source = "a=(1,2,)\n'안녕'";
/// let locator = Locator::new(source);
/// let offset = locator.convert_row_and_column(1, 2).unwrap();
/// assert_eq!(&source[TextRange::new(offset, source.text_len())], "녕'");
///
/// let source = "a=(1,2,)\n'감기'";
/// let locator = Locator::new(source);
/// let offset = locator.convert_row_and_column(1, 4).unwrap();
/// assert_eq!(&source[TextRange::new(offset, source.text_len())], "기'");
/// ```
pub fn convert_row_and_column(&self, row: usize, column: usize) -> Option<TextSize> {
let line_start = *self.to_index().line_starts().get(row)?;
let next_line_start = self
.to_index()
.line_starts()
.get(row + 1)
.copied()
.unwrap_or(self.contents.text_len());
let line_contents = &self.contents[TextRange::from(line_start..next_line_start)];
debug_assert!(
line_contents
.chars()
// Since the range goes to the next line start, `line_contents` contains the line
// break
.take_while(|c| *c != '\n' && *c != '\r')
.count()
>= column,
"The column is not in the line"
);
let len_in_line: TextSize = line_contents
.chars()
.take(column)
.map(TextLen::text_len)
.sum();
Some(line_start + len_in_line)
}
/// Take the source code between the given [`TextRange`].
#[inline]
pub fn slice<T: Ranged>(&self, ranged: T) -> &'a str {

View File

@@ -14,7 +14,7 @@ use ruff_linter::settings::{flags, DUMMY_VARIABLE_RGX, PREFIXES};
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::{Mod, PySourceType};
use ruff_python_codegen::Stylist;
use ruff_python_formatter::{format_node, pretty_comments, PyFormatContext};
use ruff_python_formatter::{format_module_ast, pretty_comments, PyFormatContext};
use ruff_python_index::{CommentRangesBuilder, Indexer};
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{parse_tokens, AsMode, Mode};
@@ -303,9 +303,9 @@ 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(
format_module_ast(
&self.module,
&self.comment_ranges,
self.source_code,

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": [
{