Compare commits

..

1 Commits

Author SHA1 Message Date
Charlie Marsh
89d9f1e124 Use PatternMatchAs for mapping rest node 2023-08-24 00:44:50 -04:00
122 changed files with 40317 additions and 37098 deletions

31
Cargo.lock generated
View File

@@ -812,7 +812,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.286"
version = "0.0.285"
dependencies = [
"anyhow",
"clap",
@@ -876,10 +876,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -2066,7 +2064,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.286"
version = "0.0.285"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2130,7 +2128,6 @@ dependencies = [
"typed-arena",
"unicode-width",
"unicode_names2",
"uuid",
"wsl",
]
@@ -2167,7 +2164,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.286"
version = "0.0.285"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2343,7 +2340,6 @@ dependencies = [
"insta",
"is-macro",
"itertools",
"memchr",
"once_cell",
"ruff_formatter",
"ruff_python_ast",
@@ -3349,26 +3345,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.4.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
dependencies = [
"getrandom",
"rand",
"uuid-macro-internal",
"wasm-bindgen",
]
[[package]]
name = "uuid-macro-internal"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7e1ba1f333bd65ce3c9f27de592fcbc256dafe3af2717f56d7c87761fbaccf4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
]
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
[[package]]
name = "valuable"

View File

@@ -50,7 +50,6 @@ tracing = "0.1.37"
tracing-indicatif = "0.3.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
unicode-width = "0.1.10"
uuid = { version = "1.4.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
# v1.0.1

View File

@@ -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.286
rev: v0.0.285
hooks:
- id: ruff
```

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.286"
version = "0.0.285"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -77,7 +77,6 @@ toml = { workspace = true }
typed-arena = { version = "2.0.2" }
unicode-width = { workspace = true }
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
[dev-dependencies]

View File

@@ -19,12 +19,3 @@ def foo(x, y, z):
class A():
pass
# b = c
dictionary = {
# "key1": 123, # noqa: ERA001
# "key2": 456,
# "key3": 789, # test
}
#import os # noqa

View File

@@ -230,10 +230,6 @@ def timedelta_okay(value=dt.timedelta(hours=1)):
def path_okay(value=Path(".")):
pass
# B008 allow arbitrary call with immutable annotation
def immutable_annotation_call(value: Sequence[int] = foo()):
pass
# B006 and B008
# We should handle arbitrary nesting of these B008.
def nested_combo(a=[float(3), dt.datetime.now()]):

View File

@@ -1,7 +1,6 @@
from typing import List
import fastapi
import custom
from fastapi import Query
@@ -17,9 +16,5 @@ def okay(data: List[str] = Query(None)):
...
def okay(data: custom.ImmutableTypeA = foo()):
...
def error_due_to_missing_import(data: List[str] = Depends(None)):
...

View File

@@ -1,3 +0,0 @@
import os
import pandas
import foo.baz

View File

@@ -1,2 +0,0 @@
[tool.ruff]
line-length = 88

View File

@@ -1,37 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"import os\n",
"\n",
"math.pi"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -1,37 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"import os\n",
"\n",
"math.pi"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -15,11 +15,9 @@
"{:s} {:y}".format("hello", "world") # [bad-format-character]
"{:*^30s}".format("centered") # OK
"{:{s}}".format("hello", s="s") # OK (nested placeholder value not checked)
"{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested placeholder format spec checked)
"{0:.{prec}g}".format(1.23, prec=15) # OK (cannot validate after nested placeholder)
"{0:.{foo}{bar}{foobar}y}".format(...) # OK (cannot validate after nested placeholders)
"{0:.{foo}x{bar}y{foobar}g}".format(...) # OK (all nested placeholders are consumed without considering in between chars)
"{:{s}}".format("hello", s="s") # OK (nested replacement value not checked)
"{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked)
## f-strings

View File

@@ -1,11 +0,0 @@
#import os # noqa
#import os # noqa: ERA001
dictionary = {
# "key1": 123, # noqa: ERA001
# "key2": 456, # noqa
# "key3": 789,
}
#import os # noqa: E501

View File

@@ -1335,23 +1335,46 @@ where
fn visit_pattern(&mut self, pattern: &'b Pattern) {
// Step 1: Binding
if let Pattern::MatchAs(ast::PatternMatchAs {
name: Some(name), ..
})
| Pattern::MatchStar(ast::PatternMatchStar {
name: Some(name),
range: _,
})
| Pattern::MatchMapping(ast::PatternMatchMapping {
rest: Some(name), ..
}) = pattern
{
self.add_binding(
name,
name.range(),
BindingKind::Assignment,
BindingFlags::empty(),
);
match &pattern {
Pattern::MatchAs(ast::PatternMatchAs {
name: Some(name),
pattern: _,
range: _,
}) => {
self.add_binding(
name,
name.range(),
BindingKind::Assignment,
BindingFlags::empty(),
);
}
Pattern::MatchStar(ast::PatternMatchStar {
name: Some(name),
range: _,
}) => {
self.add_binding(
name,
name.range(),
BindingKind::Assignment,
BindingFlags::empty(),
);
}
Pattern::MatchMapping(ast::PatternMatchMapping {
rest: Some(rest), ..
}) => {
if let Pattern::MatchAs(ast::PatternMatchAs {
name: Some(name), ..
}) = rest.as_ref()
{
self.add_binding(
name,
name.range(),
BindingKind::Assignment,
BindingFlags::empty(),
);
}
}
_ => {}
}
// Step 2: Traversal

View File

@@ -3,14 +3,14 @@
/// When we lint a jupyter notebook, we have to translate the row/column based on
/// [`ruff_text_size::TextSize`] to jupyter notebook cell/row/column.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NotebookIndex {
pub struct JupyterIndex {
/// Enter a row (1-based), get back the cell (1-based)
pub(super) row_to_cell: Vec<u32>,
/// Enter a row (1-based), get back the row in cell (1-based)
pub(super) row_to_row_in_cell: Vec<u32>,
}
impl NotebookIndex {
impl JupyterIndex {
/// Returns the cell number (1-based) for the given row (1-based).
pub fn cell(&self, row: usize) -> Option<u32> {
self.row_to_cell.get(row).copied()

View File

@@ -9,7 +9,6 @@ use itertools::Itertools;
use once_cell::sync::OnceCell;
use serde::Serialize;
use serde_json::error::Category;
use uuid::Uuid;
use ruff_diagnostics::Diagnostic;
use ruff_python_parser::lexer::lex;
@@ -18,7 +17,7 @@ use ruff_source_file::{NewlineWithTrailingNewline, UniversalNewlineIterator};
use ruff_text_size::{TextRange, TextSize};
use crate::autofix::source_map::{SourceMap, SourceMarker};
use crate::jupyter::index::NotebookIndex;
use crate::jupyter::index::JupyterIndex;
use crate::jupyter::schema::{Cell, RawNotebook, SortAlphabetically, SourceValue};
use crate::rules::pycodestyle::rules::SyntaxError;
use crate::IOError;
@@ -83,8 +82,8 @@ impl Cell {
Cell::Code(cell) => &cell.source,
_ => return false,
};
// Ignore cells containing cell magic as they act on the entire cell
// as compared to line magic which acts on a single line.
// Ignore cells containing cell magic. This is different from line magic
// which is allowed and ignored by the parser.
!match source {
SourceValue::String(string) => string
.lines()
@@ -107,7 +106,7 @@ pub struct Notebook {
source_code: String,
/// The index of the notebook. This is used to map between the concatenated
/// source code and the original notebook.
index: OnceCell<NotebookIndex>,
index: OnceCell<JupyterIndex>,
/// The raw notebook i.e., the deserialized version of JSON string.
raw: RawNotebook,
/// The offsets of each cell in the concatenated source code. This includes
@@ -157,7 +156,7 @@ impl Notebook {
TextRange::default(),
)
})?;
let mut raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
let raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
Ok(notebook) => notebook,
Err(err) => {
// Translate the error into a diagnostic
@@ -263,23 +262,6 @@ impl Notebook {
cell_offsets.push(current_offset);
}
// Add cell ids to 4.5+ notebooks if they are missing
// https://github.com/astral-sh/ruff/issues/6834
// https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md#required-field
if raw_notebook.nbformat == 4 && raw_notebook.nbformat_minor >= 5 {
for cell in &mut raw_notebook.cells {
let id = match cell {
Cell::Code(cell) => &mut cell.id,
Cell::Markdown(cell) => &mut cell.id,
Cell::Raw(cell) => &mut cell.id,
};
if id.is_none() {
// https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md#questions
*id = Some(Uuid::new_v4().to_string());
}
}
}
Ok(Self {
raw: raw_notebook,
index: OnceCell::new(),
@@ -386,7 +368,7 @@ impl Notebook {
///
/// The index building is expensive as it needs to go through the content of
/// every valid code cell.
fn build_index(&self) -> NotebookIndex {
fn build_index(&self) -> JupyterIndex {
let mut row_to_cell = vec![0];
let mut row_to_row_in_cell = vec![0];
@@ -413,7 +395,7 @@ impl Notebook {
row_to_row_in_cell.extend(1..=line_count);
}
NotebookIndex {
JupyterIndex {
row_to_cell,
row_to_row_in_cell,
}
@@ -431,7 +413,7 @@ impl Notebook {
/// The index is built only once when required. This is only used to
/// report diagnostics, so by that time all of the autofixes must have
/// been applied if `--fix` was passed.
pub(crate) fn index(&self) -> &NotebookIndex {
pub(crate) fn index(&self) -> &JupyterIndex {
self.index.get_or_init(|| self.build_index())
}
@@ -491,14 +473,12 @@ mod tests {
use anyhow::Result;
use test_case::test_case;
use crate::jupyter::index::NotebookIndex;
use crate::jupyter::index::JupyterIndex;
use crate::jupyter::schema::Cell;
use crate::jupyter::Notebook;
use crate::registry::Rule;
use crate::source_kind::SourceKind;
use crate::test::{
read_jupyter_notebook, test_contents, test_notebook_path, test_resource_path,
TestedNotebook,
read_jupyter_notebook, test_notebook_path, test_resource_path, TestedNotebook,
};
use crate::{assert_messages, settings};
@@ -579,7 +559,7 @@ print("after empty cells")
);
assert_eq!(
notebook.index(),
&NotebookIndex {
&JupyterIndex {
row_to_cell: vec![0, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 5, 7, 7, 8],
row_to_row_in_cell: vec![0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 1, 1, 2, 1],
}
@@ -679,28 +659,4 @@ print("after empty cells")
Ok(())
}
// Version <4.5, don't emit cell ids
#[test_case(Path::new("no_cell_id.ipynb"), false; "no_cell_id")]
// Version 4.5, cell ids are missing and need to be added
#[test_case(Path::new("add_missing_cell_id.ipynb"), true; "add_missing_cell_id")]
fn test_cell_id(path: &Path, has_id: bool) -> Result<()> {
let source_notebook = read_jupyter_notebook(path)?;
let source_kind = SourceKind::IpyNotebook(source_notebook);
let (_, transformed) = test_contents(
&source_kind,
path,
&settings::Settings::for_rule(Rule::UnusedImport),
);
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
let mut writer = Vec::new();
linted_notebook.write_inner(&mut writer)?;
let actual = String::from_utf8(writer)?;
if has_id {
assert!(actual.contains(r#""id": ""#));
} else {
assert!(!actual.contains(r#""id":"#));
}
Ok(())
}
}

View File

@@ -150,7 +150,6 @@ pub struct CodeCell {
/// Technically, id isn't required (it's not even present) in schema v4.0 through v4.4, but
/// it's required in v4.5. Main issue is that pycharm creates notebooks without an id
/// <https://youtrack.jetbrains.com/issue/PY-59438/Jupyter-notebooks-created-with-PyCharm-are-missing-the-id-field-in-cells-in-the-.ipynb-json>
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// Cell-level metadata.
pub metadata: Value,

View File

@@ -147,7 +147,7 @@ pub fn check_path(
match ruff_python_parser::parse_program_tokens(
tokens,
&path.to_string_lossy(),
source_type.is_ipynb(),
source_type.is_jupyter(),
) {
Ok(python_ast) => {
if use_ast {

View File

@@ -7,7 +7,7 @@ use colored::Colorize;
use ruff_source_file::OneIndexed;
use crate::fs::relativize_path;
use crate::jupyter::{Notebook, NotebookIndex};
use crate::jupyter::{JupyterIndex, Notebook};
use crate::message::diff::calculate_print_width;
use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
use crate::message::{
@@ -92,7 +92,7 @@ struct DisplayGroupedMessage<'a> {
show_source: bool,
row_length: NonZeroUsize,
column_length: NonZeroUsize,
jupyter_index: Option<&'a NotebookIndex>,
jupyter_index: Option<&'a JupyterIndex>,
}
impl Display for DisplayGroupedMessage<'_> {

View File

@@ -11,7 +11,7 @@ use ruff_source_file::{OneIndexed, SourceLocation};
use ruff_text_size::{TextRange, TextSize};
use crate::fs::relativize_path;
use crate::jupyter::{Notebook, NotebookIndex};
use crate::jupyter::{JupyterIndex, Notebook};
use crate::line_width::{LineWidth, TabSize};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
@@ -161,7 +161,7 @@ impl Display for RuleCodeAndBody<'_> {
pub(super) struct MessageCodeFrame<'a> {
pub(crate) message: &'a Message,
pub(crate) jupyter_index: Option<&'a NotebookIndex>,
pub(crate) jupyter_index: Option<&'a JupyterIndex>,
}
impl Display for MessageCodeFrame<'_> {

View File

@@ -28,7 +28,7 @@ static HASH_NUMBER: Lazy<Regex> = Lazy::new(|| Regex::new(r"#\d").unwrap());
static MULTILINE_ASSIGNMENT_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s*([(\[]\s*)?(\w+\s*,\s*)*\w+\s*([)\]]\s*)?=.*[(\[{]$").unwrap());
static PARTIAL_DICTIONARY_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^\s*['"]\w+['"]\s*:.+[,{]\s*(#.*)?$"#).unwrap());
Lazy::new(|| Regex::new(r#"^\s*['"]\w+['"]\s*:.+[,{]\s*$"#).unwrap());
static PRINT_RETURN_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(print|return)\b\s*").unwrap());
/// Returns `true` if a comment contains Python code.

View File

@@ -105,47 +105,5 @@ ERA001.py:21:5: ERA001 [*] Found commented-out code
19 19 | class A():
20 20 | pass
21 |- # b = c
22 21 |
23 22 |
24 23 | dictionary = {
ERA001.py:26:5: ERA001 [*] Found commented-out code
|
24 | dictionary = {
25 | # "key1": 123, # noqa: ERA001
26 | # "key2": 456,
| ^^^^^^^^^^^^^^ ERA001
27 | # "key3": 789, # test
28 | }
|
= help: Remove commented-out code
Possible fix
23 23 |
24 24 | dictionary = {
25 25 | # "key1": 123, # noqa: ERA001
26 |- # "key2": 456,
27 26 | # "key3": 789, # test
28 27 | }
29 28 |
ERA001.py:27:5: ERA001 [*] Found commented-out code
|
25 | # "key1": 123, # noqa: ERA001
26 | # "key2": 456,
27 | # "key3": 789, # test
| ^^^^^^^^^^^^^^^^^^^^^^ ERA001
28 | }
|
= help: Remove commented-out code
Possible fix
24 24 | dictionary = {
25 25 | # "key1": 123, # noqa: ERA001
26 26 | # "key2": 456,
27 |- # "key3": 789, # test
28 27 | }
29 28 |
30 29 | #import os # noqa

View File

@@ -99,7 +99,6 @@ mod tests {
extend_immutable_calls: vec![
"fastapi.Depends".to_string(),
"fastapi.Query".to_string(),
"custom.ImmutableTypeA".to_string(),
],
},
..Settings::for_rule(Rule::FunctionCallInDefaultArgument)

View File

@@ -7,9 +7,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::{compose_call_path, from_qualified_name, CallPath};
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_semantic::analyze::typing::{
is_immutable_annotation, is_immutable_func, is_mutable_func,
};
use ruff_python_semantic::analyze::typing::{is_immutable_func, is_mutable_func};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
@@ -22,13 +20,6 @@ use crate::checkers::ast::Checker;
/// once, at definition time. The returned value will then be reused by all
/// calls to the function, which can lead to unexpected behaviour.
///
/// Calls can be marked as an exception to this rule with the
/// [`flake8-bugbear.extend-immutable-calls`] configuration option.
///
/// Arguments with immutable type annotations will be ignored by this rule.
/// Types outside of the standard library can be marked as immutable with the
/// [`flake8-bugbear.extend-immutable-calls`] configuration option as well.
///
/// ## Example
/// ```python
/// def create_list() -> list[int]:
@@ -69,14 +60,14 @@ impl Violation for FunctionCallInDefaultArgument {
}
}
struct ArgumentDefaultVisitor<'a, 'b> {
semantic: &'a SemanticModel<'b>,
extend_immutable_calls: &'a [CallPath<'b>],
struct ArgumentDefaultVisitor<'a> {
semantic: &'a SemanticModel<'a>,
extend_immutable_calls: Vec<CallPath<'a>>,
diagnostics: Vec<(DiagnosticKind, TextRange)>,
}
impl<'a, 'b> ArgumentDefaultVisitor<'a, 'b> {
fn new(semantic: &'a SemanticModel<'b>, extend_immutable_calls: &'a [CallPath<'b>]) -> Self {
impl<'a> ArgumentDefaultVisitor<'a> {
fn new(semantic: &'a SemanticModel<'a>, extend_immutable_calls: Vec<CallPath<'a>>) -> Self {
Self {
semantic,
extend_immutable_calls,
@@ -85,12 +76,15 @@ impl<'a, 'b> ArgumentDefaultVisitor<'a, 'b> {
}
}
impl Visitor<'_> for ArgumentDefaultVisitor<'_, '_> {
fn visit_expr(&mut self, expr: &Expr) {
impl<'a, 'b> Visitor<'b> for ArgumentDefaultVisitor<'b>
where
'b: 'a,
{
fn visit_expr(&mut self, expr: &'b Expr) {
match expr {
Expr::Call(ast::ExprCall { func, .. }) => {
if !is_mutable_func(func, self.semantic)
&& !is_immutable_func(func, self.semantic, self.extend_immutable_calls)
&& !is_immutable_func(func, self.semantic, &self.extend_immutable_calls)
{
self.diagnostics.push((
FunctionCallInDefaultArgument {
@@ -120,28 +114,25 @@ pub(crate) fn function_call_in_argument_default(checker: &mut Checker, parameter
.iter()
.map(|target| from_qualified_name(target))
.collect();
let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), &extend_immutable_calls);
for ParameterWithDefault {
default,
parameter,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
if !parameter.annotation.as_ref().is_some_and(|expr| {
is_immutable_annotation(expr, checker.semantic(), &extend_immutable_calls)
}) {
let diagnostics = {
let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), extend_immutable_calls);
for ParameterWithDefault {
default,
parameter: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
visitor.visit_expr(expr);
}
}
}
for (check, range) in visitor.diagnostics {
visitor.diagnostics
};
for (check, range) in diagnostics {
checker.diagnostics.push(Diagnostic::new(check, range));
}
}

View File

@@ -258,139 +258,161 @@ B006_B008.py:114:33: B006 [*] Do not use mutable data structures for argument de
116 118 |
117 119 |
B006_B008.py:239:20: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:235:20: B006 [*] Do not use mutable data structures for argument defaults
|
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
233 | # B006 and B008
234 | # We should handle arbitrary nesting of these B008.
235 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
240 | pass
236 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
236 236 |
237 237 | # B006 and B008
238 238 | # We should handle arbitrary nesting of these B008.
239 |-def nested_combo(a=[float(3), dt.datetime.now()]):
239 |+def nested_combo(a=None):
240 |+ if a is None:
241 |+ a = [float(3), dt.datetime.now()]
240 242 | pass
241 243 |
242 244 |
232 232 |
233 233 | # B006 and B008
234 234 | # We should handle arbitrary nesting of these B008.
235 |-def nested_combo(a=[float(3), dt.datetime.now()]):
235 |+def nested_combo(a=None):
236 |+ if a is None:
237 |+ a = [float(3), dt.datetime.now()]
236 238 | pass
237 239 |
238 240 |
B006_B008.py:276:27: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:272:27: B006 [*] Do not use mutable data structures for argument defaults
|
275 | def mutable_annotations(
276 | a: list[int] | None = [],
271 | def mutable_annotations(
272 | a: list[int] | None = [],
| ^^ B006
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
= help: Replace with `None`; initialize within function
Possible fix
273 273 |
274 274 |
275 275 | def mutable_annotations(
276 |- a: list[int] | None = [],
276 |+ a: list[int] | None = None,
277 277 | b: Optional[Dict[int, int]] = {},
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 280 | ):
281 |+ if a is None:
282 |+ a = []
281 283 | pass
282 284 |
283 285 |
269 269 |
270 270 |
271 271 | def mutable_annotations(
272 |- a: list[int] | None = [],
272 |+ a: list[int] | None = None,
273 273 | b: Optional[Dict[int, int]] = {},
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 276 | ):
277 |+ if a is None:
278 |+ a = []
277 279 | pass
278 280 |
279 281 |
B006_B008.py:277:35: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:273:35: B006 [*] Do not use mutable data structures for argument defaults
|
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
271 | def mutable_annotations(
272 | a: list[int] | None = [],
273 | b: Optional[Dict[int, int]] = {},
| ^^ B006
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
= help: Replace with `None`; initialize within function
Possible fix
274 274 |
275 275 | def mutable_annotations(
276 276 | a: list[int] | None = [],
277 |- b: Optional[Dict[int, int]] = {},
277 |+ b: Optional[Dict[int, int]] = None,
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 280 | ):
281 |+ if b is None:
282 |+ b = {}
281 283 | pass
282 284 |
283 285 |
270 270 |
271 271 | def mutable_annotations(
272 272 | a: list[int] | None = [],
273 |- b: Optional[Dict[int, int]] = {},
273 |+ b: Optional[Dict[int, int]] = None,
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 276 | ):
277 |+ if b is None:
278 |+ b = {}
277 279 | pass
278 280 |
279 281 |
B006_B008.py:278:62: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:274:62: B006 [*] Do not use mutable data structures for argument defaults
|
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
272 | a: list[int] | None = [],
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 | ):
|
= help: Replace with `None`; initialize within function
Possible fix
275 275 | def mutable_annotations(
276 276 | a: list[int] | None = [],
277 277 | b: Optional[Dict[int, int]] = {},
278 |- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
278 |+ c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 280 | ):
281 |+ if c is None:
282 |+ c = set()
281 283 | pass
282 284 |
283 285 |
271 271 | def mutable_annotations(
272 272 | a: list[int] | None = [],
273 273 | b: Optional[Dict[int, int]] = {},
274 |- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
274 |+ c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 276 | ):
277 |+ if c is None:
278 |+ c = set()
277 279 | pass
278 280 |
279 281 |
B006_B008.py:279:80: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:275:80: B006 [*] Do not use mutable data structures for argument defaults
|
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
280 | ):
281 | pass
276 | ):
277 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
276 276 | a: list[int] | None = [],
277 277 | b: Optional[Dict[int, int]] = {},
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 |- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 |+ d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
280 280 | ):
281 |+ if d is None:
282 |+ d = set()
281 283 | pass
272 272 | a: list[int] | None = [],
273 273 | b: Optional[Dict[int, int]] = {},
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 |- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 |+ d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
276 276 | ):
277 |+ if d is None:
278 |+ d = set()
277 279 | pass
278 280 |
279 281 |
B006_B008.py:280:52: B006 [*] Do not use mutable data structures for argument defaults
|
280 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
281 | """Docstring"""
|
= help: Replace with `None`; initialize within function
Possible fix
277 277 | pass
278 278 |
279 279 |
280 |-def single_line_func_wrong(value: dict[str, str] = {}):
280 |+def single_line_func_wrong(value: dict[str, str] = None):
281 281 | """Docstring"""
282 |+ if value is None:
283 |+ value = {}
282 284 |
283 285 |
284 286 | def single_line_func_wrong(value: dict[str, str] = {}):
B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument defaults
|
284 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
285 | """Docstring"""
286 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
281 281 | pass
281 281 | """Docstring"""
282 282 |
283 283 |
284 |-def single_line_func_wrong(value: dict[str, str] = {}):
@@ -398,81 +420,59 @@ B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument de
285 285 | """Docstring"""
286 |+ if value is None:
287 |+ value = {}
286 288 |
286 288 | ...
287 289 |
288 290 | def single_line_func_wrong(value: dict[str, str] = {}):
288 290 |
B006_B008.py:288:52: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:289:52: B006 Do not use mutable data structures for argument defaults
|
288 | def single_line_func_wrong(value: dict[str, str] = {}):
289 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
289 | """Docstring"""
290 | ...
290 | """Docstring"""; ...
|
= help: Replace with `None`; initialize within function
Possible fix
285 285 | """Docstring"""
286 286 |
287 287 |
288 |-def single_line_func_wrong(value: dict[str, str] = {}):
288 |+def single_line_func_wrong(value: dict[str, str] = None):
289 289 | """Docstring"""
290 |+ if value is None:
291 |+ value = {}
290 292 | ...
291 293 |
292 294 |
B006_B008.py:293:52: B006 Do not use mutable data structures for argument defaults
|
293 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
294 | """Docstring"""; ...
294 | """Docstring"""; \
295 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:297:52: B006 Do not use mutable data structures for argument defaults
B006_B008.py:298:52: B006 [*] Do not use mutable data structures for argument defaults
|
297 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
298 | """Docstring"""; \
299 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:302:52: B006 [*] Do not use mutable data structures for argument defaults
|
302 | def single_line_func_wrong(value: dict[str, str] = {
298 | def single_line_func_wrong(value: dict[str, str] = {
| ____________________________________________________^
303 | | # This is a comment
304 | | }):
299 | | # This is a comment
300 | | }):
| |_^ B006
305 | """Docstring"""
301 | """Docstring"""
|
= help: Replace with `None`; initialize within function
Possible fix
299 299 | ...
300 300 |
301 301 |
302 |-def single_line_func_wrong(value: dict[str, str] = {
303 |- # This is a comment
304 |-}):
302 |+def single_line_func_wrong(value: dict[str, str] = None):
305 303 | """Docstring"""
304 |+ if value is None:
305 |+ value = {}
306 306 |
307 307 |
308 308 | def single_line_func_wrong(value: dict[str, str] = {}) \
295 295 | ...
296 296 |
297 297 |
298 |-def single_line_func_wrong(value: dict[str, str] = {
299 |- # This is a comment
300 |-}):
298 |+def single_line_func_wrong(value: dict[str, str] = None):
301 299 | """Docstring"""
300 |+ if value is None:
301 |+ value = {}
302 302 |
303 303 |
304 304 | def single_line_func_wrong(value: dict[str, str] = {}) \
B006_B008.py:308:52: B006 Do not use mutable data structures for argument defaults
B006_B008.py:304:52: B006 Do not use mutable data structures for argument defaults
|
308 | def single_line_func_wrong(value: dict[str, str] = {}) \
304 | def single_line_func_wrong(value: dict[str, str] = {}) \
| ^^ B006
309 | : \
310 | """Docstring"""
305 | : \
306 | """Docstring"""
|
= help: Replace with `None`; initialize within function

View File

@@ -46,38 +46,38 @@ B006_B008.py:134:30: B008 Do not perform function call in argument defaults
135 | ...
|
B006_B008.py:239:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:235:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
233 | # B006 and B008
234 | # We should handle arbitrary nesting of these B008.
235 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^ B008
240 | pass
236 | pass
|
B006_B008.py:245:22: B008 Do not perform function call `map` in argument defaults
B006_B008.py:241:22: B008 Do not perform function call `map` in argument defaults
|
243 | # Don't flag nested B006 since we can't guarantee that
244 | # it isn't made mutable by the outer operation.
245 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
239 | # Don't flag nested B006 since we can't guarantee that
240 | # it isn't made mutable by the outer operation.
241 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
246 | pass
242 | pass
|
B006_B008.py:250:19: B008 Do not perform function call `random.randint` in argument defaults
B006_B008.py:246:19: B008 Do not perform function call `random.randint` in argument defaults
|
249 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
245 | # B008-ception.
246 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
251 | pass
247 | pass
|
B006_B008.py:250:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:246:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
249 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
245 | # B008-ception.
246 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^ B008
251 | pass
247 | pass
|

View File

@@ -1,11 +1,11 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B008_extended.py:24:51: B008 Do not perform function call `Depends` in argument defaults
B008_extended.py:19:51: B008 Do not perform function call `Depends` in argument defaults
|
24 | def error_due_to_missing_import(data: List[str] = Depends(None)):
19 | def error_due_to_missing_import(data: List[str] = Depends(None)):
| ^^^^^^^^^^^^^ B008
25 | ...
20 | ...
|

View File

@@ -283,7 +283,6 @@ pub(crate) fn typing_only_runtime_import(
None,
&checker.settings.src,
checker.package(),
checker.settings.isort.detect_same_package,
&checker.settings.isort.known_modules,
checker.settings.target_version,
) {

View File

@@ -69,7 +69,6 @@ pub(crate) fn categorize<'a>(
level: Option<u32>,
src: &[PathBuf],
package: Option<&Path>,
detect_same_package: bool,
known_modules: &'a KnownModules,
target_version: PythonVersion,
) -> &'a ImportSection {
@@ -89,7 +88,7 @@ pub(crate) fn categorize<'a>(
&ImportSection::Known(ImportType::StandardLibrary),
Reason::KnownStandardLibrary,
)
} else if detect_same_package && same_package(package, module_base) {
} else if same_package(package, module_base) {
(
&ImportSection::Known(ImportType::FirstParty),
Reason::SamePackage,
@@ -138,7 +137,6 @@ pub(crate) fn categorize_imports<'a>(
block: ImportBlock<'a>,
src: &[PathBuf],
package: Option<&Path>,
detect_same_package: bool,
known_modules: &'a KnownModules,
target_version: PythonVersion,
) -> BTreeMap<&'a ImportSection, ImportBlock<'a>> {
@@ -150,7 +148,6 @@ pub(crate) fn categorize_imports<'a>(
None,
src,
package,
detect_same_package,
known_modules,
target_version,
);
@@ -167,7 +164,6 @@ pub(crate) fn categorize_imports<'a>(
import_from.level,
src,
package,
detect_same_package,
known_modules,
target_version,
);
@@ -184,7 +180,6 @@ pub(crate) fn categorize_imports<'a>(
import_from.level,
src,
package,
detect_same_package,
known_modules,
target_version,
);
@@ -201,7 +196,6 @@ pub(crate) fn categorize_imports<'a>(
import_from.level,
src,
package,
detect_same_package,
known_modules,
target_version,
);

View File

@@ -82,7 +82,6 @@ pub(crate) fn format_imports(
force_to_top: &BTreeSet<String>,
known_modules: &KnownModules,
order_by_type: bool,
detect_same_package: bool,
relative_imports_order: RelativeImportsOrder,
single_line_exclusions: &BTreeSet<String>,
split_on_trailing_comma: bool,
@@ -130,7 +129,6 @@ pub(crate) fn format_imports(
force_to_top,
known_modules,
order_by_type,
detect_same_package,
relative_imports_order,
split_on_trailing_comma,
classes,
@@ -189,7 +187,6 @@ fn format_import_block(
force_to_top: &BTreeSet<String>,
known_modules: &KnownModules,
order_by_type: bool,
detect_same_package: bool,
relative_imports_order: RelativeImportsOrder,
split_on_trailing_comma: bool,
classes: &BTreeSet<String>,
@@ -201,14 +198,7 @@ fn format_import_block(
section_order: &[ImportSection],
) -> String {
// Categorize by type (e.g., first-party vs. third-party).
let mut block_by_type = categorize_imports(
block,
src,
package,
detect_same_package,
known_modules,
target_version,
);
let mut block_by_type = categorize_imports(block, src, package, known_modules, target_version);
let mut output = String::new();
@@ -1094,38 +1084,4 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn detect_same_package() -> Result<()> {
let diagnostics = test_path(
Path::new("isort/detect_same_package/foo/bar.py"),
&Settings {
src: vec![],
isort: super::settings::Settings {
detect_same_package: true,
..super::settings::Settings::default()
},
..Settings::for_rule(Rule::UnsortedImports)
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn no_detect_same_package() -> Result<()> {
let diagnostics = test_path(
Path::new("isort/detect_same_package/foo/bar.py"),
&Settings {
src: vec![],
isort: super::settings::Settings {
detect_same_package: false,
..super::settings::Settings::default()
},
..Settings::for_rule(Rule::UnsortedImports)
},
)?;
assert_messages!(diagnostics);
Ok(())
}
}

View File

@@ -134,7 +134,6 @@ pub(crate) fn organize_imports(
&settings.isort.force_to_top,
&settings.isort.known_modules,
settings.isort.order_by_type,
settings.isort.detect_same_package,
settings.isort.relative_imports_order,
&settings.isort.single_line_exclusions,
settings.isort.split_on_trailing_comma,

View File

@@ -305,21 +305,6 @@ pub struct Options {
)]
/// Override in which order the sections should be output. Can be used to move custom sections.
pub section_order: Option<Vec<ImportSection>>,
#[option(
default = r#"true"#,
value_type = "bool",
example = r#"
detect-same-package = false
"#
)]
/// Whether to automatically mark imports from within the same package as first-party.
/// For example, when `detect-same-package = true`, then when analyzing files within the
/// `foo` package, any imports from within the `foo` package will be considered first-party.
///
/// This heuristic is often unnecessary when `src` is configured to detect all first-party
/// sources; however, if `src` is _not_ configured, this heuristic can be useful to detect
/// first-party imports from _within_ (but not _across_) first-party packages.
pub detect_same_package: Option<bool>,
// Tables are required to go last.
#[option(
default = "{}",
@@ -346,7 +331,6 @@ pub struct Settings {
pub force_wrap_aliases: bool,
pub force_to_top: BTreeSet<String>,
pub known_modules: KnownModules,
pub detect_same_package: bool,
pub order_by_type: bool,
pub relative_imports_order: RelativeImportsOrder,
pub single_line_exclusions: BTreeSet<String>,
@@ -368,7 +352,6 @@ impl Default for Settings {
combine_as_imports: false,
force_single_line: false,
force_sort_within_sections: false,
detect_same_package: true,
case_sensitive: false,
force_wrap_aliases: false,
force_to_top: BTreeSet::new(),
@@ -526,7 +509,6 @@ impl TryFrom<Options> for Settings {
force_sort_within_sections: options.force_sort_within_sections.unwrap_or(false),
case_sensitive: options.case_sensitive.unwrap_or(false),
force_wrap_aliases: options.force_wrap_aliases.unwrap_or(false),
detect_same_package: options.detect_same_package.unwrap_or(true),
force_to_top: BTreeSet::from_iter(options.force_to_top.unwrap_or_default()),
known_modules: KnownModules::new(
known_first_party,
@@ -613,7 +595,6 @@ impl From<Settings> for Options {
force_sort_within_sections: Some(settings.force_sort_within_sections),
case_sensitive: Some(settings.case_sensitive),
force_wrap_aliases: Some(settings.force_wrap_aliases),
detect_same_package: Some(settings.detect_same_package),
force_to_top: Some(settings.force_to_top.into_iter().collect()),
known_first_party: Some(
settings

View File

@@ -1,19 +0,0 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
bar.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / import os
2 | | import pandas
3 | | import foo.baz
|
= help: Organize imports
Fix
1 1 | import os
2 |+
2 3 | import pandas
4 |+
3 5 | import foo.baz

View File

@@ -1,19 +0,0 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
bar.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / import os
2 | | import pandas
3 | | import foo.baz
|
= help: Organize imports
Fix
1 1 | import os
2 |-import pandas
2 |+
3 3 | import foo.baz
4 |+import pandas

View File

@@ -70,7 +70,7 @@ pub(crate) fn yield_outside_function(checker: &mut Checker, expr: &Expr) {
// `await` is allowed at the top level of a Jupyter notebook.
// See: https://ipython.readthedocs.io/en/stable/interactive/autoawait.html.
if scope.kind.is_module()
&& checker.source_type.is_ipynb()
&& checker.source_type.is_jupyter()
&& keyword == DeferralKeyword::Await
{
return;

View File

@@ -63,14 +63,13 @@ pub(crate) fn call(checker: &mut Checker, string: &str, range: TextRange) {
));
}
Err(_) => {}
Ok(FormatSpec::Static(_)) => {}
Ok(FormatSpec::Dynamic(format_spec)) => {
for placeholder in format_spec.placeholders {
let FormatPart::Field { format_spec, .. } = placeholder else {
Ok(format_spec) => {
for replacement in format_spec.replacements() {
let FormatPart::Field { format_spec, .. } = replacement else {
continue;
};
if let Err(FormatSpecError::InvalidFormatType) =
FormatSpec::parse(&format_spec)
FormatSpec::parse(format_spec)
{
checker.diagnostics.push(Diagnostic::new(
BadStringFormatCharacter {

View File

@@ -51,14 +51,14 @@ bad_string_format_character.py:15:1: PLE1300 Unsupported format character 'y'
17 | "{:*^30s}".format("centered") # OK
|
bad_string_format_character.py:19:1: PLE1300 Unsupported format character 'y'
bad_string_format_character.py:20:1: PLE1300 Unsupported format character 'y'
|
17 | "{:*^30s}".format("centered") # OK
18 | "{:{s}}".format("hello", s="s") # OK (nested placeholder value not checked)
19 | "{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested placeholder format spec checked)
18 | "{:{s}}".format("hello", s="s") # OK (nested replacement value not checked)
19 |
20 | "{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1300
20 | "{0:.{prec}g}".format(1.23, prec=15) # OK (cannot validate after nested placeholder)
21 | "{0:.{foo}{bar}{foobar}y}".format(...) # OK (cannot validate after nested placeholders)
21 |
22 | ## f-strings
|

View File

@@ -165,21 +165,6 @@ mod tests {
Ok(())
}
#[test]
fn ruf100_5() -> Result<()> {
let diagnostics = test_path(
Path::new("ruff/RUF100_5.py"),
&settings::Settings {
..settings::Settings::for_rules(vec![
Rule::UnusedNOQA,
Rule::LineTooLong,
Rule::CommentedOutCode,
])
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn flake8_noqa() -> Result<()> {
let diagnostics = test_path(

View File

@@ -1,50 +0,0 @@
---
source: crates/ruff/src/rules/ruff/mod.rs
---
RUF100_5.py:7:5: ERA001 [*] Found commented-out code
|
5 | # "key1": 123, # noqa: ERA001
6 | # "key2": 456, # noqa
7 | # "key3": 789,
| ^^^^^^^^^^^^^^ ERA001
8 | }
|
= help: Remove commented-out code
Possible fix
4 4 | dictionary = {
5 5 | # "key1": 123, # noqa: ERA001
6 6 | # "key2": 456, # noqa
7 |- # "key3": 789,
8 7 | }
9 8 |
10 9 |
RUF100_5.py:11:1: ERA001 [*] Found commented-out code
|
11 | #import os # noqa: E501
| ^^^^^^^^^^^^^^^^^^^^^^^^ ERA001
|
= help: Remove commented-out code
Possible fix
8 8 | }
9 9 |
10 10 |
11 |-#import os # noqa: E501
RUF100_5.py:11:13: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
11 | #import os # noqa: E501
| ^^^^^^^^^^^^ RUF100
|
= help: Remove unused `noqa` directive
Fix
8 8 | }
9 9 |
10 10 |
11 |-#import os # noqa: E501
11 |+#import os

View File

@@ -4,13 +4,13 @@ use crate::jupyter::Notebook;
#[derive(Clone, Debug, PartialEq, is_macro::Is)]
pub enum SourceKind {
Python(String),
IpyNotebook(Notebook),
Jupyter(Notebook),
}
impl SourceKind {
/// Return the [`Notebook`] if the source kind is [`SourceKind::IpyNotebook`].
/// Return the [`Notebook`] if the source kind is [`SourceKind::Jupyter`].
pub fn notebook(&self) -> Option<&Notebook> {
if let Self::IpyNotebook(notebook) = self {
if let Self::Jupyter(notebook) = self {
Some(notebook)
} else {
None
@@ -20,10 +20,10 @@ impl SourceKind {
#[must_use]
pub(crate) fn updated(&self, new_source: String, source_map: &SourceMap) -> Self {
match self {
SourceKind::IpyNotebook(notebook) => {
SourceKind::Jupyter(notebook) => {
let mut cloned = notebook.clone();
cloned.update(source_map, new_source);
SourceKind::IpyNotebook(cloned)
SourceKind::Jupyter(cloned)
}
SourceKind::Python(_) => SourceKind::Python(new_source),
}
@@ -32,7 +32,7 @@ impl SourceKind {
pub fn source_code(&self) -> &str {
match self {
SourceKind::Python(source) => source,
SourceKind::IpyNotebook(notebook) => notebook.source_code(),
SourceKind::Jupyter(notebook) => notebook.source_code(),
}
}
}

View File

@@ -69,10 +69,10 @@ pub(crate) fn test_notebook_path(
) -> Result<TestedNotebook> {
let source_notebook = read_jupyter_notebook(path.as_ref())?;
let source_kind = SourceKind::IpyNotebook(source_notebook);
let source_kind = SourceKind::Jupyter(source_notebook);
let (messages, transformed) = test_contents(&source_kind, path.as_ref(), settings);
let expected_notebook = read_jupyter_notebook(expected.as_ref())?;
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
let linted_notebook = transformed.into_owned().expect_jupyter();
assert_eq!(
linted_notebook.cell_offsets(),
@@ -86,7 +86,7 @@ pub(crate) fn test_notebook_path(
Ok(TestedNotebook {
messages,
source_notebook: source_kind.expect_ipy_notebook(),
source_notebook: source_kind.expect_jupyter(),
linted_notebook,
})
}
@@ -112,7 +112,7 @@ pub(crate) fn max_iterations() -> usize {
/// A convenient wrapper around [`check_path`], that additionally
/// asserts that autofixes converge after a fixed number of iterations.
pub(crate) fn test_contents<'a>(
fn test_contents<'a>(
source_kind: &'a SourceKind,
path: &Path,
settings: &Settings,

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.286"
version = "0.0.285"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -293,7 +293,7 @@ pub(crate) fn lint_path(
SourceKind::Python(transformed) => {
write(path, transformed.as_bytes())?;
}
SourceKind::IpyNotebook(notebook) => {
SourceKind::Jupyter(notebook) => {
notebook.write(path)?;
}
},
@@ -308,10 +308,10 @@ pub(crate) fn lint_path(
stdout.write_all(b"\n")?;
stdout.flush()?;
}
SourceKind::IpyNotebook(dest_notebook) => {
SourceKind::Jupyter(dest_notebook) => {
// We need to load the notebook again, since we might've
// mutated it.
let src_notebook = source_kind.as_ipy_notebook().unwrap();
let src_notebook = source_kind.as_jupyter().unwrap();
let mut stdout = io::stdout().lock();
for ((idx, src_cell), dest_cell) in src_notebook
.cells()
@@ -409,7 +409,7 @@ pub(crate) fn lint_path(
);
}
let notebooks = if let SourceKind::IpyNotebook(notebook) = source_kind {
let notebooks = if let SourceKind::Jupyter(notebook) = source_kind {
FxHashMap::from_iter([(
path.to_str()
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?
@@ -567,9 +567,9 @@ impl LintSources {
let source_type = PySourceType::from(path);
// Read the file from disk.
if source_type.is_ipynb() {
if source_type.is_jupyter() {
let notebook = notebook_from_path(path).map_err(SourceExtractionError::Diagnostics)?;
let source_kind = SourceKind::IpyNotebook(notebook);
let source_kind = SourceKind::Jupyter(notebook);
Ok(LintSources {
source_type,
source_kind,
@@ -593,10 +593,10 @@ impl LintSources {
) -> Result<LintSources, SourceExtractionError> {
let source_type = path.map(PySourceType::from).unwrap_or_default();
if source_type.is_ipynb() {
if source_type.is_jupyter() {
let notebook = notebook_from_source_code(&source_code, path)
.map_err(SourceExtractionError::Diagnostics)?;
let source_kind = SourceKind::IpyNotebook(notebook);
let source_kind = SourceKind::Jupyter(notebook);
Ok(LintSources {
source_type,
source_kind,

View File

@@ -207,15 +207,8 @@ pub(crate) struct Args {
pub(crate) fn main(args: &Args) -> anyhow::Result<ExitCode> {
setup_logging(&args.log_level_args, args.log_file.as_deref())?;
let mut error_file = match &args.error_file {
Some(error_file) => Some(BufWriter::new(
File::create(error_file).context("Couldn't open error file")?,
)),
None => None,
};
let all_success = if args.multi_project {
format_dev_multi_project(args, error_file)?
format_dev_multi_project(args)?
} else {
let result = format_dev_project(&args.files, args.stability_check, args.write)?;
let error_count = result.error_count();
@@ -223,9 +216,6 @@ pub(crate) fn main(args: &Args) -> anyhow::Result<ExitCode> {
if result.error_count() > 0 {
error!(parent: None, "{}", result.display(args.format));
}
if let Some(error_file) = &mut error_file {
write!(error_file, "{}", result.display(args.format)).unwrap();
}
info!(
parent: None,
"Done: {} stability errors, {} files, similarity index {:.5}), took {:.2}s, {} input files contained syntax errors ",
@@ -291,10 +281,7 @@ fn setup_logging(log_level_args: &LogLevelArgs, log_file: Option<&Path>) -> io::
}
/// Checks a directory of projects
fn format_dev_multi_project(
args: &Args,
mut error_file: Option<BufWriter<File>>,
) -> anyhow::Result<bool> {
fn format_dev_multi_project(args: &Args) -> anyhow::Result<bool> {
let mut total_errors = 0;
let mut total_files = 0;
let mut total_syntax_error_in_input = 0;
@@ -320,6 +307,13 @@ fn format_dev_multi_project(
pb_span.pb_set_length(project_paths.len() as u64);
let pb_span_enter = pb_span.enter();
let mut error_file = match &args.error_file {
Some(error_file) => Some(BufWriter::new(
File::create(error_file).context("Couldn't open error file")?,
)),
None => None,
};
let mut results = Vec::new();
for project_path in project_paths {
@@ -350,6 +344,7 @@ fn format_dev_multi_project(
}
if let Some(error_file) = &mut error_file {
write!(error_file, "{}", result.display(args.format)).unwrap();
error_file.flush().unwrap();
}
results.push(result);

View File

@@ -2534,17 +2534,17 @@ impl<'a, Context> BestFitting<'a, Context> {
impl<Context> Format<Context> for BestFitting<'_, Context> {
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
let mut buffer = VecBuffer::new(f.state_mut());
let variants = self.variants.items();
let mut formatted_variants = Vec::with_capacity(variants.len());
for variant in variants {
let mut buffer = VecBuffer::with_capacity(8, f.state_mut());
buffer.write_element(FormatElement::Tag(StartEntry));
buffer.write_fmt(Arguments::from(variant))?;
buffer.write_element(FormatElement::Tag(EndEntry));
formatted_variants.push(buffer.into_vec().into_boxed_slice());
formatted_variants.push(buffer.take_vec().into_boxed_slice());
}
// SAFETY: The constructor guarantees that there are always at least two variants. It's, therefore,

View File

@@ -1,5 +1,4 @@
use crate::prelude::TagKind;
use crate::GroupId;
use ruff_text_size::TextRange;
use std::error::Error;
@@ -87,9 +86,7 @@ pub enum InvalidDocumentError {
/// Text
/// EndGroup
/// ```
StartTagMissing {
kind: TagKind,
},
StartTagMissing { kind: TagKind },
/// Expected a specific start tag but instead is:
/// - at the end of the document
@@ -99,10 +96,6 @@ pub enum InvalidDocumentError {
expected_start: TagKind,
actual: ActualStart,
},
UnknownGroupId {
group_id: GroupId,
},
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
@@ -155,9 +148,6 @@ impl std::fmt::Display for InvalidDocumentError {
}
}
}
InvalidDocumentError::UnknownGroupId { group_id } => {
std::write!(f, "Encountered unknown group id {group_id:?}. Ensure that the group with the id {group_id:?} exists and that the group is a parent of or comes before the element referring to it.")
}
}
}
}

View File

@@ -122,12 +122,8 @@ impl Document {
expands
}
let mut enclosing = Vec::with_capacity(if self.is_empty() {
0
} else {
self.len().ilog2() as usize
});
let mut interned = FxHashMap::default();
let mut enclosing: Vec<Enclosing> = Vec::new();
let mut interned: FxHashMap<&Interned, bool> = FxHashMap::default();
propagate_expands(self, &mut enclosing, &mut interned);
}
@@ -661,17 +657,18 @@ impl Format<IrFormatContext<'_>> for ContentArrayEnd {
impl FormatElements for [FormatElement] {
fn will_break(&self) -> bool {
use Tag::{EndLineSuffix, StartLineSuffix};
let mut ignore_depth = 0usize;
for element in self {
match element {
// Line suffix
// Ignore if any of its content breaks
FormatElement::Tag(Tag::StartLineSuffix | Tag::StartFitsExpanded(_)) => {
FormatElement::Tag(StartLineSuffix) => {
ignore_depth += 1;
}
FormatElement::Tag(Tag::EndLineSuffix | Tag::EndFitsExpanded) => {
ignore_depth = ignore_depth.saturating_sub(1);
FormatElement::Tag(EndLineSuffix) => {
ignore_depth -= 1;
}
FormatElement::Interned(interned) if ignore_depth == 0 => {
if interned.will_break() {

View File

@@ -1,11 +1,9 @@
use std::num::NonZeroU32;
use std::sync::atomic::{AtomicU32, Ordering};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub struct DebugGroupId {
value: NonZeroU32,
#[cfg_attr(feature = "serde", serde(skip))]
name: &'static str,
}
@@ -30,7 +28,6 @@ impl std::fmt::Debug for DebugGroupId {
/// See [`crate::Formatter::group_id`] on how to get a unique id.
#[repr(transparent)]
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ReleaseGroupId {
value: NonZeroU32,
}

View File

@@ -60,13 +60,11 @@ impl<'a> Printer<'a> {
let mut stack = PrintCallStack::new(PrintElementArgs::new(Indention::Level(indent)));
let mut queue: PrintQueue<'a> = PrintQueue::new(document.as_ref());
loop {
if let Some(element) = queue.pop() {
self.print_element(&mut stack, &mut queue, element)?;
} else {
if !self.flush_line_suffixes(&mut queue, &mut stack, None) {
break;
}
while let Some(element) = queue.pop() {
self.print_element(&mut stack, &mut queue, element)?;
if queue.is_empty() {
self.flush_line_suffixes(&mut queue, &mut stack, None);
}
}
@@ -146,40 +144,23 @@ impl<'a> Printer<'a> {
}
FormatElement::Tag(StartGroup(group)) => {
let print_mode = match group.mode() {
GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded,
GroupMode::Flat => {
self.flat_group_print_mode(TagKind::Group, group.id(), args, queue, stack)?
}
};
let print_mode =
self.print_group(TagKind::Group, group.mode(), args, queue, stack)?;
if let Some(id) = group.id() {
self.state.group_modes.insert_print_mode(id, print_mode);
}
stack.push(TagKind::Group, args.with_print_mode(print_mode));
}
FormatElement::Tag(StartConditionalGroup(group)) => {
let condition = group.condition();
let expected_mode = match condition.group_id {
None => args.mode(),
Some(id) => self.state.group_modes.get_print_mode(id)?,
Some(id) => self.state.group_modes.unwrap_print_mode(id, element),
};
if expected_mode == condition.mode {
let print_mode = match group.mode() {
GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded,
GroupMode::Flat => self.flat_group_print_mode(
TagKind::ConditionalGroup,
None,
args,
queue,
stack,
)?,
};
stack.push(TagKind::ConditionalGroup, args.with_print_mode(print_mode));
self.print_group(TagKind::ConditionalGroup, group.mode(), args, queue, stack)?;
} else {
// Condition isn't met, render as normal content
stack.push(TagKind::ConditionalGroup, args);
@@ -212,7 +193,7 @@ impl<'a> Printer<'a> {
FormatElement::Tag(StartConditionalContent(Condition { mode, group_id })) => {
let group_mode = match group_id {
None => args.mode(),
Some(id) => self.state.group_modes.get_print_mode(*id)?,
Some(id) => self.state.group_modes.unwrap_print_mode(*id, element),
};
if *mode == group_mode {
@@ -223,7 +204,7 @@ impl<'a> Printer<'a> {
}
FormatElement::Tag(StartIndentIfGroupBreaks(group_id)) => {
let group_mode = self.state.group_modes.get_print_mode(*group_id)?;
let group_mode = self.state.group_modes.unwrap_print_mode(*group_id, element);
let args = match group_mode {
PrintMode::Flat => args,
@@ -256,7 +237,9 @@ impl<'a> Printer<'a> {
let condition_met = match condition {
Some(condition) => {
let group_mode = match condition.group_id {
Some(group_id) => self.state.group_modes.get_print_mode(group_id)?,
Some(group_id) => {
self.state.group_modes.unwrap_print_mode(group_id, element)
}
None => args.mode(),
};
@@ -306,46 +289,47 @@ impl<'a> Printer<'a> {
result
}
fn flat_group_print_mode(
fn print_group(
&mut self,
kind: TagKind,
id: Option<GroupId>,
mode: GroupMode,
args: PrintElementArgs,
queue: &PrintQueue<'a>,
stack: &mut PrintCallStack,
) -> PrintResult<PrintMode> {
let print_mode = match args.mode() {
PrintMode::Flat if self.state.measured_group_fits => {
// A parent group has already verified that this group fits on a single line
// Thus, just continue in flat mode
PrintMode::Flat
}
// The printer is either in expanded mode or it's necessary to re-measure if the group fits
// because the printer printed a line break
_ => {
self.state.measured_group_fits = true;
let group_mode = match mode {
GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded,
GroupMode::Flat => {
match args.mode() {
PrintMode::Flat if self.state.measured_group_fits => {
// A parent group has already verified that this group fits on a single line
// Thus, just continue in flat mode
PrintMode::Flat
}
// The printer is either in expanded mode or it's necessary to re-measure if the group fits
// because the printer printed a line break
_ => {
self.state.measured_group_fits = true;
if let Some(id) = id {
self.state
.group_modes
.insert_print_mode(id, PrintMode::Flat);
}
// Measure to see if the group fits up on a single line. If that's the case,
// print the group in "flat" mode, otherwise continue in expanded mode
stack.push(kind, args.with_print_mode(PrintMode::Flat));
let fits = self.fits(queue, stack)?;
stack.pop(kind)?;
// Measure to see if the group fits up on a single line. If that's the case,
// print the group in "flat" mode, otherwise continue in expanded mode
stack.push(kind, args.with_print_mode(PrintMode::Flat));
let fits = self.fits(queue, stack)?;
stack.pop(kind)?;
if fits {
PrintMode::Flat
} else {
PrintMode::Expanded
if fits {
PrintMode::Flat
} else {
PrintMode::Expanded
}
}
}
}
};
Ok(print_mode)
stack.push(kind, args.with_print_mode(group_mode));
Ok(group_mode)
}
fn print_text(&mut self, text: &str, source_range: Option<TextRange>) {
@@ -415,7 +399,7 @@ impl<'a> Printer<'a> {
queue: &mut PrintQueue<'a>,
stack: &mut PrintCallStack,
line_break: Option<&'a FormatElement>,
) -> bool {
) {
let suffixes = self.state.line_suffixes.take_pending();
if suffixes.len() > 0 {
@@ -439,10 +423,6 @@ impl<'a> Printer<'a> {
}
}
}
true
} else {
false
}
}
@@ -777,7 +757,7 @@ struct PrinterState<'a> {
// Re-used queue to measure if a group fits. Optimisation to avoid re-allocating a new
// vec every time a group gets measured
fits_stack: Vec<StackFrame>,
fits_queue: Vec<std::slice::Iter<'a, FormatElement>>,
fits_queue: Vec<&'a [FormatElement]>,
}
impl<'a> PrinterState<'a> {
@@ -805,15 +785,17 @@ impl GroupModes {
self.0[index] = Some(mode);
}
fn get_print_mode(&self, group_id: GroupId) -> PrintResult<PrintMode> {
fn get_print_mode(&self, group_id: GroupId) -> Option<PrintMode> {
let index = u32::from(group_id) as usize;
self.0
.get(index)
.and_then(|option| option.as_ref().copied())
}
match self.0.get(index) {
Some(Some(print_mode)) => Ok(*print_mode),
None | Some(None) => Err(PrintError::InvalidDocument(
InvalidDocumentError::UnknownGroupId { group_id },
)),
}
fn unwrap_print_mode(&self, group_id: GroupId, next_element: &FormatElement) -> PrintMode {
self.get_print_mode(group_id).unwrap_or_else(|| {
panic!("Expected group with id {group_id:?} to exist but it wasn't present in the document. Ensure that a group with such a document appears in the document before the element {next_element:?}.")
})
}
}
@@ -1141,7 +1123,10 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
let print_mode = match condition.group_id {
None => args.mode(),
Some(group_id) => self.group_modes().get_print_mode(group_id)?,
Some(group_id) => self
.group_modes()
.get_print_mode(group_id)
.unwrap_or_else(|| args.mode()),
};
if condition.mode == print_mode {
@@ -1158,7 +1143,10 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
FormatElement::Tag(StartConditionalContent(condition)) => {
let print_mode = match condition.group_id {
None => args.mode(),
Some(group_id) => self.group_modes().get_print_mode(group_id)?,
Some(group_id) => self
.group_modes()
.get_print_mode(group_id)
.unwrap_or_else(|| args.mode()),
};
if condition.mode == print_mode {
@@ -1169,7 +1157,10 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
}
FormatElement::Tag(StartIndentIfGroupBreaks(id)) => {
let print_mode = self.group_modes().get_print_mode(*id)?;
let print_mode = self
.group_modes()
.get_print_mode(*id)
.unwrap_or_else(|| args.mode());
match print_mode {
PrintMode::Flat => {
@@ -1200,7 +1191,10 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
let condition_met = match condition {
Some(condition) => {
let group_mode = match condition.group_id {
Some(group_id) => self.group_modes().get_print_mode(group_id)?,
Some(group_id) => self
.group_modes()
.get_print_mode(group_id)
.unwrap_or_else(|| args.mode()),
None => args.mode(),
};
@@ -1256,17 +1250,17 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
fn fits_group(
&mut self,
kind: TagKind,
group_mode: GroupMode,
mode: GroupMode,
id: Option<GroupId>,
args: PrintElementArgs,
) -> Fits {
if self.must_be_flat && !group_mode.is_flat() {
if self.must_be_flat && !mode.is_flat() {
return Fits::No;
}
// Continue printing groups in expanded mode if measuring a `best_fitting` element where
// a group expands.
let print_mode = if group_mode.is_flat() {
let print_mode = if mode.is_flat() {
args.mode()
} else {
PrintMode::Expanded

View File

@@ -1,5 +1,6 @@
use crate::format_element::tag::TagKind;
use crate::prelude::Tag;
use crate::printer::stack::{Stack, StackedStack};
use crate::printer::{invalid_end_tag, invalid_start_tag};
use crate::{FormatElement, PrintResult};
use std::fmt::Debug;
@@ -8,11 +9,43 @@ use std::marker::PhantomData;
/// Queue of [`FormatElement`]s.
pub(super) trait Queue<'a> {
type Stack: Stack<&'a [FormatElement]>;
fn stack(&self) -> &Self::Stack;
fn stack_mut(&mut self) -> &mut Self::Stack;
fn next_index(&self) -> usize;
fn set_next_index(&mut self, index: usize);
/// Pops the element at the end of the queue.
fn pop(&mut self) -> Option<&'a FormatElement>;
fn pop(&mut self) -> Option<&'a FormatElement> {
match self.stack().top() {
Some(top_slice) => {
// SAFETY: Safe because queue ensures that slices inside `slices` are never empty.
let next_index = self.next_index();
let element = &top_slice[next_index];
if next_index + 1 == top_slice.len() {
self.stack_mut().pop().unwrap();
self.set_next_index(0);
} else {
self.set_next_index(next_index + 1);
}
Some(element)
}
None => None,
}
}
/// Returns the next element, not traversing into [`FormatElement::Interned`].
fn top_with_interned(&self) -> Option<&'a FormatElement>;
fn top_with_interned(&self) -> Option<&'a FormatElement> {
self.stack()
.top()
.map(|top_slice| &top_slice[self.next_index()])
}
/// Returns the next element, recursively resolving the first element of [`FormatElement::Interned`].
fn top(&self) -> Option<&'a FormatElement> {
@@ -31,10 +64,29 @@ pub(super) trait Queue<'a> {
}
/// Queues a slice of elements to process before the other elements in this queue.
fn extend_back(&mut self, elements: &'a [FormatElement]);
fn extend_back(&mut self, elements: &'a [FormatElement]) {
match elements {
[] => {
// Don't push empty slices
}
slice => {
let next_index = self.next_index();
let stack = self.stack_mut();
if let Some(top) = stack.pop() {
stack.push(&top[next_index..]);
}
stack.push(slice);
self.set_next_index(0);
}
}
}
/// Removes top slice.
fn pop_slice(&mut self) -> Option<&'a [FormatElement]>;
fn pop_slice(&mut self) -> Option<&'a [FormatElement]> {
self.set_next_index(0);
self.stack_mut().pop()
}
/// Skips all content until it finds the corresponding end tag with the given kind.
fn skip_content(&mut self, kind: TagKind)
@@ -60,58 +112,45 @@ pub(super) trait Queue<'a> {
/// Queue with the elements to print.
#[derive(Debug, Default, Clone)]
pub(super) struct PrintQueue<'a> {
element_slices: Vec<std::slice::Iter<'a, FormatElement>>,
slices: Vec<&'a [FormatElement]>,
next_index: usize,
}
impl<'a> PrintQueue<'a> {
pub(super) fn new(slice: &'a [FormatElement]) -> Self {
let slices = match slice {
[] => Vec::default(),
slice => vec![slice],
};
Self {
element_slices: if slice.is_empty() {
Vec::new()
} else {
vec![slice.iter()]
},
slices,
next_index: 0,
}
}
pub(super) fn is_empty(&self) -> bool {
self.slices.is_empty()
}
}
impl<'a> Queue<'a> for PrintQueue<'a> {
fn pop(&mut self) -> Option<&'a FormatElement> {
let elements = self.element_slices.last_mut()?;
elements.next().or_else(|| {
self.element_slices.pop();
let elements = self.element_slices.last_mut()?;
elements.next()
})
type Stack = Vec<&'a [FormatElement]>;
fn stack(&self) -> &Self::Stack {
&self.slices
}
fn top_with_interned(&self) -> Option<&'a FormatElement> {
let mut slices = self.element_slices.iter().rev();
let slice = slices.next()?;
match slice.as_slice().first() {
Some(element) => Some(element),
None => {
if let Some(next_elements) = slices.next() {
next_elements.as_slice().first()
} else {
None
}
}
}
fn stack_mut(&mut self) -> &mut Self::Stack {
&mut self.slices
}
fn extend_back(&mut self, elements: &'a [FormatElement]) {
if !elements.is_empty() {
self.element_slices.push(elements.iter());
}
fn next_index(&self) -> usize {
self.next_index
}
/// Removes top slice.
fn pop_slice(&mut self) -> Option<&'a [FormatElement]> {
self.element_slices
.pop()
.map(|elements| elements.as_slice())
fn set_next_index(&mut self, index: usize) {
self.next_index = index;
}
}
@@ -122,63 +161,45 @@ impl<'a> Queue<'a> for PrintQueue<'a> {
#[must_use]
#[derive(Debug)]
pub(super) struct FitsQueue<'a, 'print> {
queue: PrintQueue<'a>,
rest_elements: std::slice::Iter<'print, std::slice::Iter<'a, FormatElement>>,
stack: StackedStack<'print, &'a [FormatElement]>,
next_index: usize,
}
impl<'a, 'print> FitsQueue<'a, 'print> {
pub(super) fn new(
rest_queue: &'print PrintQueue<'a>,
queue_vec: Vec<std::slice::Iter<'a, FormatElement>>,
print_queue: &'print PrintQueue<'a>,
saved: Vec<&'a [FormatElement]>,
) -> Self {
let stack = StackedStack::with_vec(&print_queue.slices, saved);
Self {
queue: PrintQueue {
element_slices: queue_vec,
},
rest_elements: rest_queue.element_slices.iter(),
stack,
next_index: print_queue.next_index,
}
}
pub(super) fn finish(self) -> Vec<std::slice::Iter<'a, FormatElement>> {
self.queue.element_slices
pub(super) fn finish(self) -> Vec<&'a [FormatElement]> {
self.stack.into_vec()
}
}
impl<'a, 'print> Queue<'a> for FitsQueue<'a, 'print> {
fn pop(&mut self) -> Option<&'a FormatElement> {
self.queue.pop().or_else(|| {
if let Some(next_slice) = self.rest_elements.next_back() {
self.queue.extend_back(next_slice.as_slice());
self.queue.pop()
} else {
None
}
})
type Stack = StackedStack<'print, &'a [FormatElement]>;
fn stack(&self) -> &Self::Stack {
&self.stack
}
fn top_with_interned(&self) -> Option<&'a FormatElement> {
self.queue.top_with_interned().or_else(|| {
if let Some(next_elements) = self.rest_elements.as_slice().last() {
next_elements.as_slice().first()
} else {
None
}
})
fn stack_mut(&mut self) -> &mut Self::Stack {
&mut self.stack
}
fn extend_back(&mut self, elements: &'a [FormatElement]) {
if !elements.is_empty() {
self.queue.extend_back(elements);
}
fn next_index(&self) -> usize {
self.next_index
}
/// Removes top slice.
fn pop_slice(&mut self) -> Option<&'a [FormatElement]> {
self.queue.pop_slice().or_else(|| {
self.rest_elements
.next_back()
.map(std::slice::Iter::as_slice)
})
fn set_next_index(&mut self, index: usize) {
self.next_index = index;
}
}

View File

@@ -35,7 +35,7 @@ impl<T> Stack<T> for Vec<T> {
#[derive(Debug, Clone)]
pub(super) struct StackedStack<'a, T> {
/// The content of the original stack.
original: std::slice::Iter<'a, T>,
original: &'a [T],
/// Items that have been pushed since the creation of this stack and aren't part of the `original` stack.
stack: Vec<T>,
@@ -49,10 +49,7 @@ impl<'a, T> StackedStack<'a, T> {
/// Creates a new stack that uses `stack` for storing its elements.
pub(super) fn with_vec(original: &'a [T], stack: Vec<T>) -> Self {
Self {
original: original.iter(),
stack,
}
Self { original, stack }
}
/// Returns the underlying `stack` vector.
@@ -66,9 +63,13 @@ where
T: Copy,
{
fn pop(&mut self) -> Option<T> {
self.stack
.pop()
.or_else(|| self.original.next_back().copied())
self.stack.pop().or_else(|| match self.original {
[rest @ .., last] => {
self.original = rest;
Some(*last)
}
_ => None,
})
}
fn push(&mut self, value: T) {
@@ -76,13 +77,11 @@ where
}
fn top(&self) -> Option<&T> {
self.stack
.last()
.or_else(|| self.original.as_slice().last())
self.stack.last().or_else(|| self.original.last())
}
fn is_empty(&self) -> bool {
self.stack.is_empty() && self.original.len() == 0
self.original.is_empty() && self.stack.is_empty()
}
}

View File

@@ -172,7 +172,7 @@ pub struct PatternMatchSequence<'a> {
pub struct PatternMatchMapping<'a> {
keys: Vec<ComparableExpr<'a>>,
patterns: Vec<ComparablePattern<'a>>,
rest: Option<&'a str>,
rest: Option<Box<ComparablePattern<'a>>>,
}
#[derive(Debug, PartialEq, Eq, Hash)]
@@ -238,7 +238,7 @@ impl<'a> From<&'a ast::Pattern> for ComparablePattern<'a> {
}) => Self::MatchMapping(PatternMatchMapping {
keys: keys.iter().map(Into::into).collect(),
patterns: patterns.iter().map(Into::into).collect(),
rest: rest.as_deref(),
rest: rest.as_ref().map(Into::into),
}),
ast::Pattern::MatchClass(ast::PatternMatchClass {
cls,

View File

@@ -273,11 +273,19 @@ where
Pattern::MatchSequence(ast::PatternMatchSequence { patterns, range: _ }) => patterns
.iter()
.any(|pattern| any_over_pattern(pattern, func)),
Pattern::MatchMapping(ast::PatternMatchMapping { keys, patterns, .. }) => {
Pattern::MatchMapping(ast::PatternMatchMapping {
keys,
patterns,
rest,
range: _,
}) => {
keys.iter().any(|key| any_over_expr(key, func))
|| patterns
.iter()
.any(|pattern| any_over_pattern(pattern, func))
|| rest
.as_ref()
.is_some_and(|rest| any_over_pattern(rest, func))
}
Pattern::MatchClass(ast::PatternMatchClass {
cls,

View File

@@ -58,7 +58,7 @@ pub enum PySourceType {
#[default]
Python,
Stub,
Ipynb,
Jupyter,
}
impl PySourceType {
@@ -70,8 +70,8 @@ impl PySourceType {
matches!(self, PySourceType::Stub)
}
pub const fn is_ipynb(&self) -> bool {
matches!(self, PySourceType::Ipynb)
pub const fn is_jupyter(&self) -> bool {
matches!(self, PySourceType::Jupyter)
}
}
@@ -79,7 +79,7 @@ impl From<&Path> for PySourceType {
fn from(path: &Path) -> Self {
match path.extension() {
Some(ext) if ext == "pyi" => PySourceType::Stub,
Some(ext) if ext == "ipynb" => PySourceType::Ipynb,
Some(ext) if ext == "ipynb" => PySourceType::Jupyter,
_ => PySourceType::Python,
}
}

View File

@@ -3204,13 +3204,16 @@ impl AstNode for ast::PatternMatchMapping {
let ast::PatternMatchMapping {
keys,
patterns,
rest,
range: _,
rest: _,
} = self;
for (key, pattern) in keys.iter().zip(patterns) {
visitor.visit_expr(key);
visitor.visit_pattern(pattern);
}
if let Some(pattern) = rest {
visitor.visit_pattern(pattern);
}
}
}
impl AstNode for ast::PatternMatchClass {

View File

@@ -1886,7 +1886,7 @@ pub struct PatternMatchMapping {
pub range: TextRange,
pub keys: Vec<Expr>,
pub patterns: Vec<Pattern>,
pub rest: Option<Identifier>,
pub rest: Option<Box<Pattern>>,
}
impl From<PatternMatchMapping> for Pattern {

View File

@@ -666,13 +666,21 @@ pub fn walk_pattern<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, pattern: &'a P
visitor.visit_pattern(pattern);
}
}
Pattern::MatchMapping(ast::PatternMatchMapping { keys, patterns, .. }) => {
Pattern::MatchMapping(ast::PatternMatchMapping {
keys,
patterns,
rest,
..
}) => {
for expr in keys {
visitor.visit_expr(expr);
}
for pattern in patterns {
visitor.visit_pattern(pattern);
}
if let Some(rest) = rest {
visitor.visit_pattern(rest);
}
}
Pattern::MatchClass(ast::PatternMatchClass {
cls,

View File

@@ -694,7 +694,7 @@ impl<'a> Generator<'a> {
if let Some(rest) = rest {
self.p_delim(&mut first, ", ");
self.p("**");
self.p_id(rest);
self.unparse_pattern(rest);
}
self.p("}");
}

View File

@@ -25,7 +25,6 @@ clap = { workspace = true }
countme = "3.0.1"
is-macro = { workspace = true }
itertools = { workspace = true }
memchr = { workspace = true }
once_cell = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true, optional = true }

View File

@@ -3,205 +3,10 @@
The goal of our formatter is to be compatible with Black except for rare edge cases (mostly
involving comment placement).
## Dev tools
## Implementing a node
**Testing your changes** You can use the `ruff_python_formatter` binary to format individual files
and show debug info. It's fast to compile because it doesn't depend on `ruff`. The easiest way is to
create a `scratch.py` (or `scratch.pyi`) in the project root and run
```shell
cargo run --bin ruff_python_formatter -- --emit stdout scratch.py
```
which has `--print-ir` and `--print-comments` options. We especially recommend `--print-comments`.
<details>
<summary>Usage example</summary>
Command
```shell
cargo run --bin ruff_python_formatter -- --emit stdout --print-comments --print-ir scratch.py
```
Input
```python
def f(): # a
pass
```
Output
```text
[
"def f",
group([group(["()"]), source_position(7)]),
":",
line_suffix([" # a"]),
expand_parent,
indent([hard_line_break, "pass", source_position(21)]),
hard_line_break,
source_position(21),
hard_line_break,
source_position(22)
]
{
Node {
kind: StmtFunctionDef,
range: 0..21,
source: `def f(): # a⏎`,
}: {
"leading": [],
"dangling": [
SourceComment {
text: "# a",
position: EndOfLine,
formatted: true,
},
],
"trailing": [],
},
}
def f(): # a
pass
```
</details>
The other option is to use the playground (also check the playground README):
```shell
cd playground && npm install && npm run dev:wasm && npm run dev
```
Run`npm run dev:wasm` and reload the page in the browser to refresh.
**Tests** Running the entire ruff test suite is slow, `cargo test -p ruff_python_formatter` is a
lot faster. We use [insta](https://insta.rs/) to create snapshots of all tests in
`crates/ruff_python_formatter/resources/test/fixtures/ruff`. We have copied the majority of tests
over from Black to check the difference between Ruff and Black output. Whenever we have no more
differences on a Black input file, the snapshot is deleted.
**Ecosystem checks** `scripts/formatter_ecosystem_checks.sh` runs Black compatibility and stability
checks on a number of selected projects. It will print the similarity index, the percentage of lines
that remains unchanged between Black's formatting and our formatting. You could compute it as the
number of neutral lines in a diff divided by the neutral plus the removed lines. We run this script
in CI, you can view the results in a PR page under "Checks" > "CI" > "Summary" at the bottom of the
page. The stability checks catch for three common problems: The second
formatting pass looks different than the first (formatter instability or lack of idempotency),
printing invalid syntax (e.g. missing parentheses around multiline expressions) and panics (mostly
in debug assertions). You should ensure that your changes don't decrease the similarity index.
**Terminology** For `()`, `[]` and `{}` we use the following terminology:
- Parentheses: `(`, `)` or all kind of parentheses (`()`, `[]` and `{}`, e.g.
`has_own_parentheses`)
- Brackets: `[`, `]`
- Braces: `{`, `}`
## `format_dev`
It's possible to format an entire project:
```shell
cargo run --bin ruff_dev -- format-dev --write /path/to/my_project
```
Available options:
- `--write`: Format the files and write them back to disk.
- `--stability-check`: Format twice (but don't write to disk without `--write`) and check for
differences and crashes.
- `--multi-project`: Treat every subdirectory as a separate project. Useful for ecosystem checks.
- `--error-file`: Write all errors to the given file.
- `--log-file`: Write all messages to the given file.
- `--stats-file`: Use together with `--multi-project`, this writes the similarity index as unicode
table to the given file.
**Large ecosystem checks** It is also possible to check a large number of repositories. This dataset
is large (~60GB), so we only do this occasionally:
```shell
# Get the list of projects
curl https://raw.githubusercontent.com/akx/ruff-usage-aggregate/master/data/known-github-tomls-clean.jsonl > github_search.jsonl
# Repurpose this script to download the repositories for us
python scripts/check_ecosystem.py --checkouts target/checkouts --projects github_search.jsonl -v $(which true) $(which true)
# Check each project for formatter stability
cargo run --bin ruff_dev -- format-dev --stability-check --error-file target/formatter-ecosystem-errors.txt --multi-project target/checkouts
```
**Shrinking** To shrink a formatter error from an entire file to a minimal reproducible example,
you can use `ruff_shrinking`:
```shell
cargo run --bin ruff_shrinking -- <your_file> target/shrinking.py "Unstable formatting" "target/debug/ruff_dev format-dev --stability-check target/shrinking.py"
```
The first argument is the input file, the second is the output file where the candidates
and the eventual minimized version will be written to. The third argument is a regex matching the
error message, e.g. "Unstable formatting" or "Formatter error". The last argument is the command
with the error, e.g. running the stability check on the candidate file. The script will try various
strategies to remove parts of the code. If the output of the command still matches, it will use that
slightly smaller code as starting point for the next iteration, otherwise it will revert and try
a different strategy until all strategies are exhausted.
## Helper structs
To abstract formatting something into a helper, create a new struct with the data you want to
format and implement `Format<PyFormatContext<'_>> for MyStruct`. Below is a small dummy example.
```rust
/// Helper to hide the fields for the struct
pub(crate) fn empty_parenthesized<'content>(
comments: &'content [SourceComment],
has_plus_prefix: bool,
) -> FormatEmptyParenthesized<'content> {
FormatEmptyParenthesized {
comments,
has_plus_prefix,
}
}
/// The wrapper struct
pub(crate) struct FormatEmptyParenthesized<'content> {
comments: &'content [SourceComment],
has_plus_prefix: bool,
}
impl Format<PyFormatContext<'_>> for FormatEmptyParenthesized<'_> {
/// Here we implement the actual formatting
fn fmt(&self, f: &mut Formatter<PyFormatContext>) -> FormatResult<()> {
if self.has_plus_prefix {
text("+").fmt(f)?; // This is equivalent to `write!(f, [text("*")])?;`
}
write!(
f,
[
text("("),
soft_block_indent(&dangling_comments(&self.comments)),
text(")")
]
)
}
}
```
If the struct is used across modules, also adds constructor function that hides the fields of the
struct. Since it implements `Format`, you can directly use it in write calls:
```rust
write!(f, [empty_parenthesized(dangling_end_of_line_comments)])?;
```
Check the `builders` module for existing primitives.
## Adding new syntax
Occasionally, Python will add new syntax. After adding it to `ruff_python_ast`, run `generate.py`
to generate stubs for node formatting. This will add a `Format{{Node}}` struct
that implements `Default` (and `AsFormat`/`IntoFormat` impls in `generated.rs`, see orphan rules
below).
Formatting each node follows roughly the same structure. We start with a `Format{{Node}}` struct
that implements Default (and `AsFormat`/`IntoFormat` impls in `generated.rs`, see orphan rules below).
```rust
#[derive(Default)]
@@ -242,6 +47,8 @@ impl FormatNodeRule<StmtReturn> for FormatStmtReturn {
}
```
Check the `builders` module for the primitives that you can use.
If something such as list or a tuple can break into multiple lines if it is too long for a single
line, wrap it into a `group`. Ignoring comments, we could format a tuple with two items like this:
@@ -370,10 +177,10 @@ the `break` and wrongly formatted as such. We can identify these cases by lookin
between two bodies that have the same indentation level as the keyword, e.g. in our case the
leading else comment is inside the `while` node (which spans the entire snippet) and on the same
level as the `else`. We identify those case in
[`handle_own_line_comment_around_body`](https://github.com/astral-sh/ruff/blob/4bdd99f8822d914a59f918fc46bbd17a88e2fe47/crates/ruff_python_formatter/src/comments/placement.rs#L390)
[`handle_in_between_bodies_own_line_comment`](https://github.com/astral-sh/ruff/blob/be11cae619d5a24adb4da34e64d3c5f270f9727b/crates/ruff_python_formatter/src/comments/placement.rs#L196)
and mark them as dangling for manual formatting later. Similarly, we find and mark comment after
the colon(s) in
[`handle_end_of_line_comment_around_body`](https://github.com/astral-sh/ruff/blob/4bdd99f8822d914a59f918fc46bbd17a88e2fe47/crates/ruff_python_formatter/src/comments/placement.rs#L238C4-L238C14)
[`handle_trailing_end_of_line_condition_comment`](https://github.com/astral-sh/ruff/blob/main/crates/ruff_python_formatter/src/comments/placement.rs#L518)
.
The comments don't carry any extra information such as why we marked the comment as trailing,
@@ -414,6 +221,95 @@ fn fmt_fields(&self, item: &StmtWhile, f: &mut PyFormatter) -> FormatResult<()>
}
```
## Development notes
Handling parentheses and comments are two major challenges in a Python formatter.
We have copied the majority of tests over from Black and use [insta](https://insta.rs/docs/cli/) for
snapshot testing with the diff between Ruff and Black, Black output and Ruff output. We put
additional test cases in `resources/test/fixtures/ruff`.
The full Ruff test suite is slow, `cargo test -p ruff_python_formatter` is a lot faster.
You can check the black compatibility on a number of projects using
`scripts/formatter_ecosystem_checks.sh`. It will print the similarity index, the percentage of lines
that remains unchanged between black's formatting and our formatting. You could compute it as the
number of neutral lines in a diff divided by the neutral plus the removed lines. It also checks for
common problems such unstable formatting, internal formatter errors and printing invalid syntax. We
run this script in CI and you can view the results in a PR page under "Checks" > "CI" > "Summary" at
the bottom of the page.
There is a `ruff_python_formatter` binary that avoid building and linking the main `ruff` crate.
You can use `scratch.py` as a playground, e.g.
`cargo run --bin ruff_python_formatter -- --emit stdout scratch.py`, which additional `--print-ir`
and `--print-comments` options.
The origin of Ruff's formatter is the [Rome formatter](https://github.com/rome/tools/tree/main/crates/rome_json_formatter),
e.g. the ruff_formatter crate is forked from the [rome_formatter crate](https://github.com/rome/tools/tree/main/crates/rome_formatter).
The Rome repository can be a helpful reference when implementing something in the Ruff formatter.
### Checking entire projects
It's possible to format an entire project:
```shell
cargo run --bin ruff_dev -- format-dev --write my_project
```
This will format all files that `ruff check` would lint and computes the similarity index, the
fraction of changed lines. The similarity index is 1 if there were no changes at all, while 0 means
we changed every single line. If you run this on a black formatted projects, this tells you how
similar the ruff formatter is to black for the given project, with our goal being as close to 1 as
possible.
There are three common problems with the formatter: The second formatting pass looks different than
the first (formatter instability or lack of idempotency), we print invalid syntax (e.g. missing
parentheses around multiline expressions) and panics (mostly in debug assertions). We test for all
of these using the `--stability-check` option in the `format-dev` subcommand:
The easiest is to check CPython:
```shell
git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff/resources/test/cpython
cargo run --bin ruff_dev -- format-dev --stability-check crates/ruff/resources/test/cpython
```
Compared to `ruff check`, `cargo run --bin ruff_dev -- format-dev` has 4 additional options:
- `--write`: Format the files and write them back to disk
- `--stability-check`: Format twice (but don't write to disk) and check for differences and crashes
- `--multi-project`: Treat every subdirectory as a separate project. Useful for ecosystem checks.
- `--error-file`: Use together with `--multi-project`, this writes all errors (but not status
messages) to a file.
It is also possible to check a large number of repositories. This dataset is large (~60GB), so we
only do this occasionally:
```shell
# Get the list of projects
curl https://raw.githubusercontent.com/akx/ruff-usage-aggregate/master/data/known-github-tomls-clean.jsonl > github_search.jsonl
# Repurpose this script to download the repositories for us
python scripts/check_ecosystem.py --checkouts target/checkouts --projects github_search.jsonl -v $(which true) $(which true)
# Check each project for formatter stability
cargo run --bin ruff_dev -- format-dev --stability-check --error-file target/formatter-ecosystem-errors.txt --multi-project target/checkouts
```
To shrink a formatter error from an entire file to a minimal reproducible example, you can use
`ruff_shrinking`:
```shell
cargo run --bin ruff_shrinking -- <your_file> target/shrinking.py "Unstable formatting" "target/release/ruff_dev format-dev --stability-check target/shrinking.py"
```
The first argument is the input file, the second is the output file where the candidates
and the eventual minimized version will be written to. The third argument is a regex matching the
error message, e.g. "Unstable formatting" or "Formatter error". The last argument is the command
with the error, e.g. running the stability check on the candidate file. The script will try various
strategies to remove parts of the code. If the output of the command still matches, it will use that
slightly smaller code as starting point for the next iteration, otherwise it will revert and try
a different strategy until all strategies are exhausted.
## The orphan rules and trait structure
For the formatter, we would like to implement `Format` from the rust_formatter crate for all AST

View File

@@ -109,19 +109,3 @@ x6 = (
# regression: https://github.com/astral-sh/ruff/issues/6181
(#
()).a
(
(
a # trailing end-of-line
# trailing own-line
) # dangling before dot end-of-line
.b # trailing end-of-line
)
(
(
a
)
# dangling before dot own-line
.b
)

View File

@@ -164,44 +164,3 @@ func(
[]
)
)
# Comments between the function and its arguments
aaa = (
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
# awkward comment
()
.bbbbbbbbbbbbbbbb
)
aaa = (
# bar
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
# awkward comment
()
.bbbbbbbbbbbbbbbb
)
aaa = (
# bar
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb # baz
# awkward comment
()
.bbbbbbbbbbbbbbbb
)
aaa = (
(foo # awkward comment
)
()
.bbbbbbbbbbbbbbbb
)
aaa = (
(
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
# awkward comment
)
()
.bbbbbbbbbbbbbbbb
)

View File

@@ -50,11 +50,3 @@ c1 = [ # trailing open bracket
second,
third
] # outer comment
[ # inner comment
# own-line comment
( # end-of-line comment
# own-line comment
first,
),
] # outer comment

View File

@@ -124,9 +124,3 @@ test_particular = [
'c'
)
}
# Regression test for https://github.com/astral-sh/ruff/issues/5893
x = ("""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa""" """bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb""")
x = (f"""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa""" f"""bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb""")
x = (b"""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa""" b"""bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb""")

View File

@@ -140,9 +140,3 @@ if not \
# Regression: https://github.com/astral-sh/ruff/issues/5338
if a and not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
if (
not
# comment
a):
...

View File

@@ -1,72 +0,0 @@
x = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
x_aa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
xxxxx = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
while (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
):
pass
while aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
pass
# Only applies in `Parenthesize::IfBreaks` positions
raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
raise (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
)
raise a from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
raise a from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
# Can never apply on expression statement level
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
# Is it only relevant for items that can't break
aaaaaaa = 111111111111111111111111111111111111111111111111111111111111111111111111111111
aaaaaaa = (
1111111111111111111111111111111111111111111111111111111111111111111111111111111
)
aaaaaaa = """111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111"""
# Never parenthesize multiline strings
aaaaaaa = (
"""1111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111"""
)
aaaaaaaa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbb
aaaaaaaa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
aaaaaaaa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
for converter in connection.ops.get_db_converters(
expression
) + expression.get_db_converters(connection):
...
aaa = (
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb # awkward comment
)
def test():
m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.ManyToManyField(Person, blank=True)
m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.ManyToManyFieldAttributeChainField
m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.ManyToManyField(Person, blank=True)
m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.ManyToManyFieldAttributeChainFieeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeld
def test():
if True:
VLM_m2m = VLM.m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.through
allows_group_by_select_index = self.connection.features.allows_group_by_select_index

View File

@@ -66,13 +66,6 @@ def foo():
1
)
yield (
"# * Make sure each ForeignKey and OneToOneField has `on_delete` set "
"to the desired behavior"
)
yield aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ccccccccccccccccccccccccccccccccccccccccccccccccccccccc
yield ("Cache key will cause errors if used with memcached: %r " "(longer than %s)" % (
key,
MEMCACHE_MAX_KEY_LENGTH,

View File

@@ -12,7 +12,7 @@ assert ( # Dangle1
assert (
# Leading test value
True # Trailing test value same-line
# Trailing test value own-line
# Trailing test value own-line
), "Some string" # Trailing msg same-line
# Trailing assert
@@ -23,133 +23,8 @@ assert (
assert (
# Leading test value
True # Trailing test value same-line
# Trailing test value own-line
# Trailing test value own-line
# Test dangler
), "Some string" # Trailing msg same-line
# Trailing assert
def test():
assert {
key1: value1,
key2: value2,
key3: value3,
key4: value4,
key5: value5,
key6: value6,
key7: value7,
key8: value8,
key9: value9,
} == expected, (
"Not what we expected and the message is too long to fit ineeeeee one line"
)
assert {
key1: value1,
key2: value2,
key3: value3,
key4: value4,
key5: value5,
key6: value6,
key7: value7,
key8: value8,
key9: value9,
} == expected, (
"Not what we expected and the message is too long to fit in one lineeeee"
)
assert {
key1: value1,
key2: value2,
key3: value3,
key4: value4,
key5: value5,
key6: value6,
key7: value7,
key8: value8,
key9: value9,
} == expected, "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeee"
assert (
{
key1: value1,
key2: value2,
key3: value3,
key4: value4,
key5: value5,
key6: value6,
key7: value7,
key8: value8,
key9: value9,
}
== expectedeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
), "Not what we expected and the message is too long to fit in one lin"
assert (
{
key1: value1,
key2: value2,
key3: value3,
key4: value4,
key5: value5,
key6: value6,
key7: value7,
key8: value8,
key9: value9,
}
== expectedeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
), "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeeeeeee"
assert expected == {
key1: value1,
key2: value2,
key3: value3,
key4: value4,
key5: value5,
key6: value6,
key7: value7,
key8: value8,
key9: value9,
}, "Not what we expected and the message is too long to fit ineeeeee one line"
assert expected == {
key1: value1,
key2: value2,
key3: value3,
key4: value4,
key5: value5,
key6: value6,
key7: value7,
key8: value8,
key9: value9,
}, "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeeeeeeeeee"
assert (
expectedeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
== {
key1: value1,
key2: value2,
key3: value3,
key4: value4,
key5: value5,
key6: value6,
key7: value7,
key8: value8,
key9: value9,
}
), "Not what we expected and the message is too long to fit in one lin"
assert (
expectedeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
== {
key1: value1,
key2: value2,
key3: value3,
key4: value4,
key5: value5,
key6: value6,
key7: value7,
key8: value8,
key9: value9,
}
), "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeeeee"

View File

@@ -263,7 +263,6 @@ match foo:
):
y = 1
match foo:
case [1, 2, *rest]:
pass
@@ -300,102 +299,3 @@ match foo:
_, 1, 2]:
pass
match foo:
case (1):
pass
case ((1)):
pass
case [(1), 2]:
pass
case [( # comment
1
), 2]:
pass
case [ # outer
( # inner
1
), 2]:
pass
case [
( # outer
[ # inner
1,
]
)
]:
pass
case [ # outer
( # inner outer
[ # inner
1,
]
)
]:
pass
case [ # outer
# own line
( # inner outer
[ # inner
1,
]
)
]:
pass
case [(*rest), (a as b)]:
pass
match foo:
case {"a": 1, "b": 2}:
pass
case {
# own line
"a": 1, # end-of-line
# own line
"b": 2,
}:
pass
case { # open
1 # key
: # colon
value # value
}:
pass
case {**d}:
pass
case {
** # middle with single item
b
}:
pass
case {
# before
** # between
b,
}:
pass
case {
1: x,
# foo
** # bop
# before
b, # boo
# baz
}:
pass
case {
1: x
# foo
,
**
b,
}:
pass

View File

@@ -5,7 +5,7 @@ use ruff_formatter::{format_args, write, FormatError, SourceCode};
use ruff_python_ast::node::{AnyNodeRef, AstNode};
use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before};
use crate::comments::{CommentLinePosition, SourceComment};
use crate::comments::SourceComment;
use crate::context::NodeLevel;
use crate::prelude::*;
@@ -206,15 +206,8 @@ impl Format<PyFormatContext<'_>> for FormatDanglingComments<'_> {
.iter()
.filter(|comment| comment.is_unformatted())
{
if first {
match comment.line_position {
CommentLinePosition::OwnLine => {
write!(f, [hard_line_break()])?;
}
CommentLinePosition::EndOfLine => {
write!(f, [space(), space()])?;
}
}
if first && comment.line_position().is_end_of_line() {
write!(f, [space(), space()])?;
}
write!(

View File

@@ -3,9 +3,7 @@ use std::cmp::Ordering;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::whitespace::indentation;
use ruff_python_ast::{self as ast, Comprehension, Expr, MatchCase, Parameters, Ranged};
use ruff_python_trivia::{
find_only_token_in_range, indentation_at_offset, SimpleToken, SimpleTokenKind, SimpleTokenizer,
};
use ruff_python_trivia::{indentation_at_offset, SimpleToken, SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::Locator;
use ruff_text_size::{TextLen, TextRange};
@@ -15,7 +13,6 @@ use crate::expression::expr_tuple::is_tuple_parenthesized;
use crate::other::parameters::{
assign_argument_separator_comment_placement, find_parameter_separators,
};
use crate::pattern::pattern_match_sequence::SequenceType;
/// Manually attach comments to nodes that the default placement gets wrong.
pub(super) fn place_comment<'a>(
@@ -180,18 +177,7 @@ fn handle_enclosed_comment<'a>(
AnyNodeRef::Comprehension(comprehension) => {
handle_comprehension_comment(comment, comprehension, locator)
}
AnyNodeRef::PatternMatchSequence(pattern_match_sequence) => {
if SequenceType::from_pattern(pattern_match_sequence, locator.contents())
.is_parenthesized()
{
handle_bracketed_end_of_line_comment(comment, locator)
} else {
CommentPlacement::Default(comment)
}
}
AnyNodeRef::ExprAttribute(attribute) => {
handle_attribute_comment(comment, attribute, locator)
}
AnyNodeRef::ExprAttribute(attribute) => handle_attribute_comment(comment, attribute),
AnyNodeRef::ExprBinOp(binary_expression) => {
handle_trailing_binary_expression_left_or_operator_comment(
comment,
@@ -221,17 +207,13 @@ fn handle_enclosed_comment<'a>(
AnyNodeRef::WithItem(_) => handle_with_item_comment(comment, locator),
AnyNodeRef::PatternMatchAs(_) => handle_pattern_match_as_comment(comment, locator),
AnyNodeRef::PatternMatchStar(_) => handle_pattern_match_star_comment(comment),
AnyNodeRef::PatternMatchMapping(pattern) => {
handle_bracketed_end_of_line_comment(comment, locator)
.or_else(|comment| handle_pattern_match_mapping_comment(comment, pattern, locator))
}
AnyNodeRef::StmtFunctionDef(_) => handle_leading_function_with_decorators_comment(comment),
AnyNodeRef::StmtClassDef(class_def) => {
handle_leading_class_with_decorators_comment(comment, class_def)
}
AnyNodeRef::StmtImportFrom(import_from) => handle_import_from_comment(comment, import_from),
AnyNodeRef::MatchCase(match_case) => handle_match_case_comment(comment, match_case),
AnyNodeRef::StmtWith(with_) => handle_with_comment(comment, with_),
AnyNodeRef::ExprCall(_) => handle_call_comment(comment),
AnyNodeRef::ExprConstant(_) => {
if let Some(AnyNodeRef::ExprFString(fstring)) = comment.enclosing_parent() {
CommentPlacement::dangling(fstring, comment)
@@ -373,7 +355,7 @@ fn is_first_statement_in_body(statement: AnyNodeRef, has_body: AnyNodeRef) -> bo
| AnyNodeRef::ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler {
body, ..
})
| AnyNodeRef::MatchCase(MatchCase { body, .. })
| AnyNodeRef::MatchCase(ast::MatchCase { body, .. })
| AnyNodeRef::StmtFunctionDef(ast::StmtFunctionDef { body, .. })
| AnyNodeRef::StmtClassDef(ast::StmtClassDef { body, .. }) => {
are_same_optional(statement, body.first())
@@ -989,114 +971,50 @@ fn handle_dict_unpacking_comment<'a>(
}
}
/// Handle comments between a function call and its arguments. For example, attach the following as
/// dangling on the call:
/// ```python
/// (
/// func
/// # dangling
/// ()
/// )
/// ```
fn handle_call_comment(comment: DecoratedComment) -> CommentPlacement {
if comment.line_position().is_own_line() {
if comment.preceding_node().is_some_and(|preceding| {
comment.following_node().is_some_and(|following| {
preceding.end() < comment.start() && comment.end() < following.start()
})
}) {
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
}
CommentPlacement::Default(comment)
}
/// Own line comments coming after the node are always dangling comments
/// ```python
/// (
/// a # trailing comment on `a`
/// # dangling comment on the attribute
/// . # dangling comment on the attribute
/// # dangling comment on the attribute
/// a
/// # trailing a comment
/// . # dangling comment
/// # or this
/// b
/// )
/// ```
fn handle_attribute_comment<'a>(
comment: DecoratedComment<'a>,
attribute: &'a ast::ExprAttribute,
locator: &Locator,
) -> CommentPlacement<'a> {
if comment.preceding_node().is_none() {
// ```text
// ( value) . attr
// ^^^^ we're in this range
// ```
return CommentPlacement::leading(attribute.value.as_ref(), comment);
}
// If the comment is parenthesized, use the parentheses to either attach it as a trailing
// comment on the value or a dangling comment on the attribute.
// For example, treat this as trailing:
// ```python
// (
// (
// value
// # comment
// )
// .attribute
// )
// ```
//
// However, treat this as dangling:
// ```python
// (
// (value)
// # comment
// .attribute
// )
// ```
if let Some(right_paren) = SimpleTokenizer::starts_at(attribute.value.end(), locator.contents())
.skip_trivia()
.take_while(|token| token.kind == SimpleTokenKind::RParen)
.last()
{
return if comment.start() < right_paren.start() {
CommentPlacement::trailing(attribute.value.as_ref(), comment)
} else {
CommentPlacement::dangling(comment.enclosing_node(), comment)
};
}
// If the comment precedes the `.`, treat it as trailing _if_ it's on the same line as the
// value. For example, treat this as trailing:
// ```python
// (
// value # comment
// .attribute
// )
// ```
//
// However, treat this as dangling:
// ```python
// (
// value
// # comment
// .attribute
// )
// ```text
// value . attr
// ^^^^^^^ we're in this range
// ```
debug_assert!(
TextRange::new(attribute.value.end(), attribute.attr.start())
.contains(comment.slice().start())
);
if comment.line_position().is_end_of_line() {
let dot_token = find_only_token_in_range(
TextRange::new(attribute.value.end(), attribute.attr.start()),
SimpleTokenKind::Dot,
locator.contents(),
);
if comment.end() < dot_token.start() {
return CommentPlacement::trailing(attribute.value.as_ref(), comment);
}
// Attach as trailing comment to a. The specific placement is only relevant for fluent style
// ```python
// x322 = (
// a
// . # end-of-line dot comment 2
// b
// )
// ```
CommentPlacement::trailing(attribute.value.as_ref(), comment)
} else {
CommentPlacement::dangling(attribute, comment)
}
CommentPlacement::dangling(comment.enclosing_node(), comment)
}
/// Assign comments between `if` and `test` and `else` and `orelse` as leading to the respective
@@ -1133,7 +1051,7 @@ fn handle_expr_if_comment<'a>(
let if_token = find_only_token_in_range(
TextRange::new(body.end(), test.start()),
SimpleTokenKind::If,
locator.contents(),
locator,
);
// Between `if` and `test`
if if_token.range.start() < comment.slice().start() && comment.slice().start() < test.start() {
@@ -1143,7 +1061,7 @@ fn handle_expr_if_comment<'a>(
let else_token = find_only_token_in_range(
TextRange::new(test.end(), orelse.start()),
SimpleTokenKind::Else,
locator.contents(),
locator,
);
// Between `else` and `orelse`
if else_token.range.start() < comment.slice().start()
@@ -1215,7 +1133,7 @@ fn handle_with_item_comment<'a>(
let as_token = find_only_token_in_range(
TextRange::new(context_expr.end(), optional_vars.start()),
SimpleTokenKind::As,
locator.contents(),
locator,
);
if comment.end() < as_token.start() {
@@ -1284,59 +1202,6 @@ fn handle_pattern_match_star_comment(comment: DecoratedComment) -> CommentPlacem
CommentPlacement::dangling(comment.enclosing_node(), comment)
}
/// Handles trailing comments after the `**` in a pattern match item. The comments can either
/// appear between the `**` and the identifier, or after the identifier (which is just an
/// identifier, not a node).
///
/// ```python
/// case {
/// ** # dangling end of line comment
/// # dangling own line comment
/// rest # dangling end of line comment
/// # dangling own line comment
/// ): ...
/// ```
fn handle_pattern_match_mapping_comment<'a>(
comment: DecoratedComment<'a>,
pattern: &'a ast::PatternMatchMapping,
locator: &Locator,
) -> CommentPlacement<'a> {
// The `**` has to come at the end, so there can't be another node after it. (The identifier,
// like `rest` above, isn't a node.)
if comment.following_node().is_some() {
return CommentPlacement::Default(comment);
};
// If there's no rest pattern, no need to do anything special.
let Some(rest) = pattern.rest.as_ref() else {
return CommentPlacement::Default(comment);
};
// If the comment falls after the `**rest` entirely, treat it as dangling on the enclosing
// node.
if comment.start() > rest.end() {
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
// Look at the tokens between the previous node (or the start of the pattern) and the comment.
let preceding_end = match comment.preceding_node() {
Some(preceding) => preceding.end(),
None => comment.enclosing_node().start(),
};
let mut tokens = SimpleTokenizer::new(
locator.contents(),
TextRange::new(preceding_end, comment.start()),
)
.skip_trivia();
// If the remaining tokens from the previous node include `**`, mark as a dangling comment.
if tokens.any(|token| token.kind == SimpleTokenKind::DoubleStar) {
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else {
CommentPlacement::Default(comment)
}
}
/// Handles comments around the `:=` token in a named expression (walrus operator).
///
/// For example, here, `# 1` and `# 2` will be marked as dangling comments on the named expression,
@@ -1367,7 +1232,7 @@ fn handle_named_expr_comment<'a>(
let colon_equal = find_only_token_in_range(
TextRange::new(target.end(), value.start()),
SimpleTokenKind::ColonEqual,
locator.contents(),
locator,
);
if comment.end() < colon_equal.start() {
@@ -1380,6 +1245,23 @@ fn handle_named_expr_comment<'a>(
}
}
/// Looks for a token in the range that contains no other tokens except for parentheses outside
/// the expression ranges
fn find_only_token_in_range(
range: TextRange,
token_kind: SimpleTokenKind,
locator: &Locator,
) -> SimpleToken {
let mut tokens = SimpleTokenizer::new(locator.contents(), range)
.skip_trivia()
.skip_while(|token| token.kind == SimpleTokenKind::RParen);
let token = tokens.next().expect("Expected a token");
debug_assert_eq!(token.kind(), token_kind);
let mut tokens = tokens.skip_while(|token| token.kind == SimpleTokenKind::LParen);
debug_assert_eq!(tokens.next(), None);
token
}
/// Attach an end-of-line comment immediately following an open bracket as a dangling comment on
/// enclosing node.
///
@@ -1479,6 +1361,28 @@ fn handle_import_from_comment<'a>(
}
}
/// Attach an enclosed end-of-line comment to a [`MatchCase`].
///
/// For example, given:
/// ```python
/// case ( # comment
/// pattern
/// ):
/// ...
/// ```
///
/// The comment will be attached to the [`MatchCase`] node as a dangling comment.
fn handle_match_case_comment<'a>(
comment: DecoratedComment<'a>,
match_case: &'a MatchCase,
) -> CommentPlacement<'a> {
if comment.line_position().is_end_of_line() && comment.start() < match_case.pattern.start() {
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else {
CommentPlacement::Default(comment)
}
}
/// Attach an enclosed end-of-line comment to a [`ast::StmtWith`].
///
/// For example, given:
@@ -1552,7 +1456,7 @@ fn handle_comprehension_comment<'a>(
let in_token = find_only_token_in_range(
TextRange::new(comprehension.target.end(), comprehension.iter.start()),
SimpleTokenKind::In,
locator.contents(),
locator,
);
// Comments between the target and the `in`
@@ -1615,7 +1519,7 @@ fn handle_comprehension_comment<'a>(
let if_token = find_only_token_in_range(
TextRange::new(last_end, if_node.start()),
SimpleTokenKind::If,
locator.contents(),
locator,
);
if is_own_line {
if last_end < comment.slice().start() && comment.slice().start() < if_token.start() {

View File

@@ -1,10 +1,8 @@
use ruff_formatter::{write, FormatRuleWithOptions};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{Constant, Expr, ExprAttribute, ExprConstant, Ranged};
use ruff_python_trivia::{find_only_token_in_range, SimpleTokenKind};
use ruff_text_size::TextRange;
use ruff_python_ast::{Constant, Expr, ExprAttribute, ExprConstant};
use crate::comments::{dangling_comments, SourceComment};
use crate::comments::{leading_comments, trailing_comments, SourceComment};
use crate::expression::parentheses::{
is_expression_parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
};
@@ -46,6 +44,13 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
})
);
let comments = f.context().comments().clone();
let dangling_comments = comments.dangling(item);
let leading_attribute_comments_start = dangling_comments
.partition_point(|comment| comment.line_position().is_end_of_line());
let (trailing_dot_comments, leading_attribute_comments) =
dangling_comments.split_at(leading_attribute_comments_start);
if needs_parentheses {
value.format().with_options(Parentheses::Always).fmt(f)?;
} else if call_chain_layout == CallChainLayout::Fluent {
@@ -83,42 +88,55 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
value.format().fmt(f)?;
}
// Identify dangling comments before and after the dot:
// ```python
// (
// (
// a
// ) # `before_dot`
// # `before_dot`
// . # `after_dot`
// # `after_dot`
// b
// )
// ```
let comments = f.context().comments().clone();
let dangling = comments.dangling(item);
let (before_dot, after_dot) = if dangling.is_empty() {
(dangling, dangling)
} else {
let dot_token = find_only_token_in_range(
TextRange::new(item.value.end(), item.attr.start()),
SimpleTokenKind::Dot,
f.context().source(),
);
dangling.split_at(
dangling.partition_point(|comment| comment.start() < dot_token.start()),
)
};
if comments.has_trailing_own_line(value.as_ref()) {
hard_line_break().fmt(f)?;
}
write!(
f,
[
dangling_comments(before_dot),
text("."),
dangling_comments(after_dot),
attr.format()
]
)
if call_chain_layout == CallChainLayout::Fluent {
// Fluent style has line breaks before the dot
// ```python
// blogs3 = (
// Blog.objects.filter(
// entry__headline__contains="Lennon",
// )
// .filter(
// entry__pub_date__year=2008,
// )
// .filter(
// entry__pub_date__year=2008,
// )
// )
// ```
write!(
f,
[
(!leading_attribute_comments.is_empty()).then_some(hard_line_break()),
leading_comments(leading_attribute_comments),
text("."),
trailing_comments(trailing_dot_comments),
attr.format()
]
)
} else {
// Regular style
// ```python
// blogs2 = Blog.objects.filter(
// entry__headline__contains="Lennon",
// ).filter(
// entry__pub_date__year=2008,
// )
// ```
write!(
f,
[
text("."),
trailing_comments(trailing_dot_comments),
(!leading_attribute_comments.is_empty()).then_some(hard_line_break()),
leading_comments(leading_attribute_comments),
attr.format()
]
)
}
});
let is_call_chain_root = self.call_chain_layout == CallChainLayout::Default
@@ -151,10 +169,14 @@ impl NeedsParentheses for ExprAttribute {
== CallChainLayout::Fluent
{
OptionalParentheses::Multiline
} else if context.comments().has_dangling(self) {
} else if context
.comments()
.dangling(self)
.iter()
.any(|comment| comment.line_position().is_own_line())
|| context.comments().has_trailing_own_line(self)
{
OptionalParentheses::Always
} else if self.value.is_name_expr() {
OptionalParentheses::BestFit
} else {
self.value.needs_parentheses(self.into(), context)
}

View File

@@ -1,10 +1,9 @@
use crate::expression::CallChainLayout;
use ruff_formatter::FormatRuleWithOptions;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{Expr, ExprCall};
use crate::comments::{dangling_comments, SourceComment};
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::expression::CallChainLayout;
use crate::prelude::*;
use crate::FormatNodeRule;
@@ -30,24 +29,16 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
arguments,
} = item;
let comments = f.context().comments().clone();
let dangling = comments.dangling(item);
let call_chain_layout = self.call_chain_layout.apply_in_node(item, f);
let fmt_func = format_with(|f| {
// Format the function expression.
let fmt_inner = format_with(|f| {
match func.as_ref() {
Expr::Attribute(expr) => expr.format().with_options(call_chain_layout).fmt(f),
Expr::Call(expr) => expr.format().with_options(call_chain_layout).fmt(f),
Expr::Subscript(expr) => expr.format().with_options(call_chain_layout).fmt(f),
_ => func.format().fmt(f),
}?;
Expr::Attribute(expr) => expr.format().with_options(call_chain_layout).fmt(f)?,
Expr::Call(expr) => expr.format().with_options(call_chain_layout).fmt(f)?,
Expr::Subscript(expr) => expr.format().with_options(call_chain_layout).fmt(f)?,
_ => func.format().fmt(f)?,
}
// Format comments between the function and its arguments.
dangling_comments(dangling).fmt(f)?;
// Format the arguments.
arguments.format().fmt(f)
});
@@ -60,19 +51,11 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
if call_chain_layout == CallChainLayout::Fluent
&& self.call_chain_layout == CallChainLayout::Default
{
group(&fmt_func).fmt(f)
group(&fmt_inner).fmt(f)
} else {
fmt_func.fmt(f)
fmt_inner.fmt(f)
}
}
fn fmt_dangling_comments(
&self,
_dangling_comments: &[SourceComment],
_f: &mut PyFormatter,
) -> FormatResult<()> {
Ok(())
}
}
impl NeedsParentheses for ExprCall {
@@ -85,8 +68,6 @@ impl NeedsParentheses for ExprCall {
== CallChainLayout::Fluent
{
OptionalParentheses::Multiline
} else if context.comments().has_dangling(self) {
OptionalParentheses::Always
} else {
self.func.needs_parentheses(self.into(), context)
}

View File

@@ -5,7 +5,7 @@ use ruff_python_ast::{Constant, ExprConstant, Ranged};
use ruff_text_size::{TextLen, TextRange};
use crate::expression::number::{FormatComplex, FormatFloat, FormatInt};
use crate::expression::parentheses::{should_use_best_fit, NeedsParentheses, OptionalParentheses};
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::expression::string::{
AnyString, FormatString, StringLayout, StringPrefix, StringQuotes,
};
@@ -80,15 +80,12 @@ impl NeedsParentheses for ExprConstant {
context: &PyFormatContext,
) -> OptionalParentheses {
if self.value.is_implicit_concatenated() {
OptionalParentheses::Multiline
} else if is_multiline_string(self, context.source())
|| self.value.is_none()
|| self.value.is_bool()
|| self.value.is_ellipsis()
{
OptionalParentheses::Never
} else if should_use_best_fit(self, context) {
OptionalParentheses::BestFit
// Don't wrap triple quoted strings
if is_multiline_string(self, context.source()) {
OptionalParentheses::Never
} else {
OptionalParentheses::Multiline
}
} else {
OptionalParentheses::Never
}
@@ -102,8 +99,7 @@ pub(super) fn is_multiline_string(constant: &ExprConstant, source: &str) -> bool
let quotes =
StringQuotes::parse(&contents[TextRange::new(prefix.text_len(), contents.text_len())]);
quotes.is_some_and(StringQuotes::is_triple)
&& memchr::memchr2(b'\n', b'\r', contents.as_bytes()).is_some()
quotes.is_some_and(StringQuotes::is_triple) && contents.contains(['\n', '\r'])
} else {
false
}

View File

@@ -1,8 +1,6 @@
use super::string::{AnyString, FormatString};
use crate::context::PyFormatContext;
use memchr::memchr2;
use crate::expression::parentheses::{should_use_best_fit, NeedsParentheses, OptionalParentheses};
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::prelude::*;
use crate::{FormatNodeRule, PyFormatter};
use ruff_formatter::FormatResult;
@@ -22,16 +20,8 @@ impl NeedsParentheses for ExprFString {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
context: &PyFormatContext,
_context: &PyFormatContext,
) -> OptionalParentheses {
if self.implicit_concatenated {
OptionalParentheses::Multiline
} else if memchr2(b'\n', b'\r', context.source()[self.range].as_bytes()).is_none()
&& should_use_best_fit(self, context)
{
OptionalParentheses::BestFit
} else {
OptionalParentheses::Never
}
OptionalParentheses::Multiline
}
}

View File

@@ -1,5 +1,5 @@
use crate::comments::SourceComment;
use crate::expression::parentheses::{should_use_best_fit, NeedsParentheses, OptionalParentheses};
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::prelude::*;
use crate::FormatNodeRule;
use ruff_formatter::{write, FormatContext};
@@ -38,13 +38,9 @@ impl NeedsParentheses for ExprName {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
context: &PyFormatContext,
_context: &PyFormatContext,
) -> OptionalParentheses {
if should_use_best_fit(self, context) {
OptionalParentheses::BestFit
} else {
OptionalParentheses::Never
}
OptionalParentheses::Never
}
}

View File

@@ -101,10 +101,7 @@ impl NeedsParentheses for ExprSubscript {
{
OptionalParentheses::Multiline
} else {
match self.value.needs_parentheses(self.into(), context) {
OptionalParentheses::BestFit => OptionalParentheses::Never,
parentheses => parentheses,
}
self.value.needs_parentheses(self.into(), context)
}
}
}

View File

@@ -3,7 +3,7 @@ use ruff_python_ast::{ExprUnaryOp, Ranged};
use ruff_text_size::{TextLen, TextRange};
use ruff_formatter::prelude::{hard_line_break, space, text};
use ruff_formatter::{Format, FormatResult};
use ruff_formatter::{Format, FormatContext, FormatResult};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
@@ -57,7 +57,7 @@ impl FormatNodeRule<ExprUnaryOp> for FormatExprUnaryOp {
// a)
// ```
if !leading_operand_comments.is_empty()
&& !is_operand_parenthesized(item, f.context().source())
&& !is_operand_parenthesized(item, f.context().source_code().as_str())
{
hard_line_break().fmt(f)?;
} else if op.is_not() {

View File

@@ -1,8 +1,7 @@
use itertools::Itertools;
use std::cmp::Ordering;
use ruff_formatter::{
format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions,
write, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions,
};
use ruff_python_ast as ast;
use ruff_python_ast::node::AnyNodeRef;
@@ -10,7 +9,6 @@ use ruff_python_ast::visitor::preorder::{walk_expr, PreorderVisitor};
use ruff_python_ast::{Expr, ExpressionRef, Operator};
use crate::builders::parenthesize_if_expands;
use crate::comments::leading_comments;
use crate::context::{NodeLevel, WithNodeLevel};
use crate::expression::parentheses::{
is_expression_parenthesized, optional_parentheses, parenthesized, NeedsParentheses,
@@ -109,6 +107,8 @@ impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
};
if parenthesize {
let comments = f.context().comments().clone();
// Any comments on the open parenthesis of a `node`.
//
// For example, `# comment` in:
@@ -117,23 +117,18 @@ impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
// foo.bar
// )
// ```
let comments = f.context().comments().clone();
let leading = comments.leading(expression);
if let Some((index, open_parenthesis_comment)) = leading
.iter()
.find_position(|comment| comment.line_position().is_end_of_line())
{
write!(
f,
[
leading_comments(&leading[..index]),
parenthesized("(", &format_expr, ")")
.with_dangling_comments(std::slice::from_ref(open_parenthesis_comment))
]
let open_parenthesis_comment = comments
.leading(expression)
.first()
.filter(|comment| comment.line_position().is_end_of_line());
parenthesized("(", &format_expr, ")")
.with_dangling_comments(
open_parenthesis_comment
.map(std::slice::from_ref)
.unwrap_or_default(),
)
} else {
parenthesized("(", &format_expr, ")").fmt(f)
}
.fmt(f)
} else {
let level = match f.context().node_level() {
NodeLevel::TopLevel | NodeLevel::CompoundStatement => NodeLevel::Expression(None),
@@ -225,64 +220,6 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
}
}
},
OptionalParentheses::BestFit => match parenthesize {
Parenthesize::IfBreaksOrIfRequired => {
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
.fmt(f)
}
Parenthesize::Optional | Parenthesize::IfRequired => {
expression.format().with_options(Parentheses::Never).fmt(f)
}
Parenthesize::IfBreaks => {
let group_id = f.group_id("optional_parentheses");
let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f);
let mut format_expression = expression
.format()
.with_options(Parentheses::Never)
.memoized();
// Don't use best fitting if it is known that the expression can never fit
if format_expression.inspect(f)?.will_break() {
// The group here is necessary because `format_expression` may contain IR elements
// that refer to the group id
group(&format_expression)
.with_group_id(Some(group_id))
.should_expand(true)
.fmt(f)
} else {
// Only add parentheses if it makes the expression fit on the line.
// Using the flat version as the most expanded version gives a left-to-right splitting behavior
// which differs from when using regular groups, because they split right-to-left.
best_fitting![
// ---------------------------------------------------------------------
// Variant 1:
// Try to fit the expression without any parentheses
group(&format_expression).with_group_id(Some(group_id)),
// ---------------------------------------------------------------------
// Variant 2:
// Try to fit the expression by adding parentheses and indenting the expression.
group(&format_args![
text("("),
soft_block_indent(&format_expression),
text(")")
])
.with_group_id(Some(group_id))
.should_expand(true),
// ---------------------------------------------------------------------
// Variant 3: Fallback, no parentheses
// Expression doesn't fit regardless of adding the parentheses. Remove the parentheses again.
group(&format_expression)
.with_group_id(Some(group_id))
.should_expand(true)
]
// Measure all lines, to avoid that the printer decides that this fits right after hitting
// the `(`.
.with_mode(BestFittingMode::AllLines)
.fmt(f)
}
}
},
OptionalParentheses::Never => match parenthesize {
Parenthesize::IfBreaksOrIfRequired => {
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
@@ -293,7 +230,6 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
expression.format().with_options(Parentheses::Never).fmt(f)
}
},
OptionalParentheses::Always => {
expression.format().with_options(Parentheses::Always).fmt(f)
}

View File

@@ -1,5 +1,5 @@
use ruff_formatter::prelude::tag::Condition;
use ruff_formatter::{format_args, write, Argument, Arguments, FormatContext, FormatOptions};
use ruff_formatter::{format_args, write, Argument, Arguments};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{ExpressionRef, Ranged};
use ruff_python_trivia::{first_non_trivia_token, SimpleToken, SimpleTokenKind, SimpleTokenizer};
@@ -15,37 +15,14 @@ pub(crate) enum OptionalParentheses {
/// Add parentheses if the expression expands over multiple lines
Multiline,
/// Always set parentheses regardless if the expression breaks or if they are
/// Always set parentheses regardless if the expression breaks or if they were
/// present in the source.
Always,
/// Add parentheses if it helps to make this expression fit. Otherwise never add parentheses.
/// This mode should only be used for expressions that don't have their own split points, e.g. identifiers,
/// or constants.
BestFit,
/// Never add parentheses. Use it for expressions that have their own parentheses or if the expression body always spans multiple lines (multiline strings).
/// Never add parentheses
Never,
}
pub(super) fn should_use_best_fit<T>(value: T, context: &PyFormatContext) -> bool
where
T: Ranged,
{
let text_len = context.source()[value.range()].len();
// Only use best fits if:
// * The text is longer than 5 characters:
// This is to align the behavior with `True` and `False`, that don't use best fits and are 5 characters long.
// It allows to avoid [`OptionalParentheses::BestFit`] for most numbers and common identifiers like `self`.
// The downside is that it can result in short values not being parenthesized if they exceed the line width.
// This is considered an edge case not worth the performance penalty and IMO, breaking an identifier
// of 5 characters to avoid it exceeding the line width by 1 reduces the readability.
// * The text is know to never fit: The text can never fit even when parenthesizing if it is longer
// than the configured line width (minus indent).
text_len > 5 && text_len < context.options().line_width().value() as usize
}
pub(crate) trait NeedsParentheses {
/// Determines if this object needs optional parentheses or if it is safe to omit the parentheses.
fn needs_parentheses(

View File

@@ -377,7 +377,7 @@ impl StringPrefix {
}
pub(super) const fn is_raw_string(self) -> bool {
self.contains(StringPrefix::RAW) || self.contains(StringPrefix::RAW_UPPER)
matches!(self, StringPrefix::RAW | StringPrefix::RAW_UPPER)
}
}

View File

@@ -249,10 +249,12 @@ if True:
#[test]
fn quick_test() {
let src = r#"
for converter in connection.ops.get_db_converters(
expression
) + expression.get_db_converters(connection):
...
@MyDecorator(list = a) # fmt: skip
# trailing comment
class Test:
pass
"#;
// Tokenize once
let mut tokens = Vec::new();
@@ -289,10 +291,9 @@ for converter in connection.ops.get_db_converters(
assert_eq!(
printed.as_code(),
r#"for converter in connection.ops.get_db_converters(
expression
) + expression.get_db_converters(connection):
...
r#"while True:
if something.changed:
do.stuff() # trailing comment
"#
);
}

View File

@@ -1,13 +1,13 @@
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::node::AstNode;
use ruff_python_ast::MatchCase;
use ruff_python_ast::{MatchCase, Pattern, Ranged};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::TextRange;
use crate::builders::parenthesize_if_expands;
use crate::comments::SourceComment;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, Parentheses};
use crate::expression::parentheses::parenthesized;
use crate::prelude::*;
use crate::statement::clause::{clause_body, clause_header, ClauseHeader};
use crate::{FormatNodeRule, PyFormatter};
use crate::{FormatError, FormatNodeRule, PyFormatter};
#[derive(Default)]
pub struct FormatMatchCase;
@@ -21,8 +21,13 @@ impl FormatNodeRule<MatchCase> for FormatMatchCase {
body,
} = item;
// Distinguish dangling comments that appear on the open parenthesis from those that
// appear on the trailing colon.
let comments = f.context().comments().clone();
let dangling_item_comments = comments.dangling(item);
let (open_parenthesis_comments, trailing_colon_comments) = dangling_item_comments.split_at(
dangling_item_comments.partition_point(|comment| comment.start() < pattern.start()),
);
write!(
f,
@@ -33,29 +38,12 @@ impl FormatNodeRule<MatchCase> for FormatMatchCase {
&format_with(|f| {
write!(f, [text("case"), space()])?;
let has_comments = comments.has_leading(pattern)
|| comments.has_trailing_own_line(pattern);
if has_comments {
pattern.format().with_options(Parentheses::Always).fmt(f)?;
if is_match_case_pattern_parenthesized(item, pattern, f.context())? {
parenthesized("(", &pattern.format(), ")")
.with_dangling_comments(open_parenthesis_comments)
.fmt(f)?;
} else {
match pattern.needs_parentheses(item.as_any_node_ref(), f.context()) {
OptionalParentheses::Multiline => {
parenthesize_if_expands(
&pattern.format().with_options(Parentheses::Never),
)
.fmt(f)?;
}
OptionalParentheses::Always => {
pattern.format().with_options(Parentheses::Always).fmt(f)?;
}
OptionalParentheses::Never => {
pattern.format().with_options(Parentheses::Never).fmt(f)?;
}
OptionalParentheses::BestFit => {
pattern.format().with_options(Parentheses::Never).fmt(f)?;
}
}
pattern.format().fmt(f)?;
}
if let Some(guard) = guard {
@@ -65,7 +53,7 @@ impl FormatNodeRule<MatchCase> for FormatMatchCase {
Ok(())
}),
),
clause_body(body, dangling_item_comments),
clause_body(body, trailing_colon_comments),
]
)
}
@@ -79,3 +67,33 @@ impl FormatNodeRule<MatchCase> for FormatMatchCase {
Ok(())
}
}
fn is_match_case_pattern_parenthesized(
case: &MatchCase,
pattern: &Pattern,
context: &PyFormatContext,
) -> FormatResult<bool> {
let mut tokenizer = SimpleTokenizer::new(
context.source(),
TextRange::new(case.start(), pattern.start()),
)
.skip_trivia();
let case_keyword = tokenizer.next().ok_or(FormatError::syntax_error(
"Expected a `case` keyword, didn't find any token",
))?;
debug_assert_eq!(
case_keyword.kind(),
SimpleTokenKind::Case,
"Expected `case` keyword but at {case_keyword:?}"
);
match tokenizer.next() {
Some(left_paren) => {
debug_assert_eq!(left_paren.kind(), SimpleTokenKind::LParen);
Ok(true)
}
None => Ok(false),
}
}

View File

@@ -1,11 +1,6 @@
use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{Pattern, Ranged};
use ruff_python_trivia::{first_non_trivia_token, SimpleToken, SimpleTokenKind, SimpleTokenizer};
use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule};
use ruff_python_ast::Pattern;
use crate::expression::parentheses::{
parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
};
use crate::prelude::*;
pub(crate) mod pattern_match_as;
@@ -17,64 +12,20 @@ pub(crate) mod pattern_match_singleton;
pub(crate) mod pattern_match_star;
pub(crate) mod pattern_match_value;
#[derive(Copy, Clone, PartialEq, Eq, Default)]
pub struct FormatPattern {
parentheses: Parentheses,
}
impl FormatRuleWithOptions<Pattern, PyFormatContext<'_>> for FormatPattern {
type Options = Parentheses;
fn with_options(mut self, options: Self::Options) -> Self {
self.parentheses = options;
self
}
}
#[derive(Default)]
pub struct FormatPattern;
impl FormatRule<Pattern, PyFormatContext<'_>> for FormatPattern {
fn fmt(&self, pattern: &Pattern, f: &mut PyFormatter) -> FormatResult<()> {
let format_pattern = format_with(|f| match pattern {
Pattern::MatchValue(pattern) => pattern.format().fmt(f),
Pattern::MatchSingleton(pattern) => pattern.format().fmt(f),
Pattern::MatchSequence(pattern) => pattern.format().fmt(f),
Pattern::MatchMapping(pattern) => pattern.format().fmt(f),
Pattern::MatchClass(pattern) => pattern.format().fmt(f),
Pattern::MatchStar(pattern) => pattern.format().fmt(f),
Pattern::MatchAs(pattern) => pattern.format().fmt(f),
Pattern::MatchOr(pattern) => pattern.format().fmt(f),
});
let parenthesize = match self.parentheses {
Parentheses::Preserve => is_pattern_parenthesized(pattern, f.context().source()),
Parentheses::Always => true,
Parentheses::Never => false,
};
if parenthesize {
let comments = f.context().comments().clone();
// Any comments on the open parenthesis.
//
// For example, `# comment` in:
// ```python
// ( # comment
// 1
// )
// ```
let open_parenthesis_comment = comments
.leading(pattern)
.first()
.filter(|comment| comment.line_position().is_end_of_line());
parenthesized("(", &format_pattern, ")")
.with_dangling_comments(
open_parenthesis_comment
.map(std::slice::from_ref)
.unwrap_or_default(),
)
.fmt(f)
} else {
format_pattern.fmt(f)
fn fmt(&self, item: &Pattern, f: &mut PyFormatter) -> FormatResult<()> {
match item {
Pattern::MatchValue(p) => p.format().fmt(f),
Pattern::MatchSingleton(p) => p.format().fmt(f),
Pattern::MatchSequence(p) => p.format().fmt(f),
Pattern::MatchMapping(p) => p.format().fmt(f),
Pattern::MatchClass(p) => p.format().fmt(f),
Pattern::MatchStar(p) => p.format().fmt(f),
Pattern::MatchAs(p) => p.format().fmt(f),
Pattern::MatchOr(p) => p.format().fmt(f),
}
}
}
@@ -83,7 +34,7 @@ impl<'ast> AsFormat<PyFormatContext<'ast>> for Pattern {
type Format<'a> = FormatRefWithRule<'a, Pattern, FormatPattern, PyFormatContext<'ast>>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(self, FormatPattern::default())
FormatRefWithRule::new(self, FormatPattern)
}
}
@@ -91,49 +42,6 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for Pattern {
type Format = FormatOwnedWithRule<Pattern, FormatPattern, PyFormatContext<'ast>>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(self, FormatPattern::default())
}
}
fn is_pattern_parenthesized(pattern: &Pattern, contents: &str) -> bool {
// First test if there's a closing parentheses because it tends to be cheaper.
if matches!(
first_non_trivia_token(pattern.end(), contents),
Some(SimpleToken {
kind: SimpleTokenKind::RParen,
..
})
) {
let mut tokenizer =
SimpleTokenizer::up_to_without_back_comment(pattern.start(), contents).skip_trivia();
matches!(
tokenizer.next_back(),
Some(SimpleToken {
kind: SimpleTokenKind::LParen,
..
})
)
} else {
false
}
}
impl NeedsParentheses for Pattern {
fn needs_parentheses(
&self,
parent: AnyNodeRef,
context: &PyFormatContext,
) -> OptionalParentheses {
match self {
Pattern::MatchValue(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchSingleton(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchSequence(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchMapping(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchClass(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchStar(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchAs(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchOr(pattern) => pattern.needs_parentheses(parent, context),
}
FormatOwnedWithRule::new(self, FormatPattern)
}
}

View File

@@ -1,9 +1,8 @@
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::PatternMatchAs;
use ruff_python_ast::{Pattern, PatternMatchAs};
use crate::comments::{dangling_comments, SourceComment};
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::expression::parentheses::parenthesized;
use crate::prelude::*;
use crate::{FormatNodeRule, PyFormatter};
@@ -22,7 +21,18 @@ impl FormatNodeRule<PatternMatchAs> for FormatPatternMatchAs {
if let Some(name) = name {
if let Some(pattern) = pattern {
pattern.format().fmt(f)?;
// Parenthesize nested `PatternMatchAs` like `(a as b) as c`.
if matches!(
pattern.as_ref(),
Pattern::MatchAs(PatternMatchAs {
pattern: Some(_),
..
})
) {
parenthesized("(", &pattern.format(), ")").fmt(f)?;
} else {
pattern.format().fmt(f)?;
}
if comments.has_trailing(pattern.as_ref()) {
write!(f, [hard_line_break()])?;
@@ -58,13 +68,3 @@ impl FormatNodeRule<PatternMatchAs> for FormatPatternMatchAs {
Ok(())
}
}
impl NeedsParentheses for PatternMatchAs {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Multiline
}
}

View File

@@ -1,9 +1,6 @@
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::PatternMatchClass;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::prelude::*;
use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter};
#[derive(Default)]
@@ -20,13 +17,3 @@ impl FormatNodeRule<PatternMatchClass> for FormatPatternMatchClass {
)
}
}
impl NeedsParentheses for PatternMatchClass {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
}
}

View File

@@ -1,187 +1,19 @@
use ruff_formatter::{format_args, write};
use ruff_python_ast::node::AnyNodeRef;
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::PatternMatchMapping;
use ruff_python_ast::{Expr, Identifier, Pattern, Ranged};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::TextRange;
use crate::comments::{leading_comments, trailing_comments, SourceComment};
use crate::expression::parentheses::{
empty_parenthesized, parenthesized, NeedsParentheses, OptionalParentheses,
};
use crate::prelude::*;
use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter};
#[derive(Default)]
pub struct FormatPatternMatchMapping;
impl FormatNodeRule<PatternMatchMapping> for FormatPatternMatchMapping {
fn fmt_fields(&self, item: &PatternMatchMapping, f: &mut PyFormatter) -> FormatResult<()> {
let PatternMatchMapping {
keys,
patterns,
rest,
range: _,
} = item;
debug_assert_eq!(keys.len(), patterns.len());
let comments = f.context().comments().clone();
let dangling = comments.dangling(item);
if keys.is_empty() && rest.is_none() {
return empty_parenthesized("{", dangling, "}").fmt(f);
}
// This node supports three kinds of dangling comments. Most of the complexity originates
// with the rest pattern (`{**rest}`), since we can have comments around the `**`, but
// also, the `**rest` itself is not a node (it's an identifier), so comments that trail it
// are _also_ dangling.
//
// Specifically, we have these three sources of dangling comments:
// ```python
// { # "open parenthesis comment"
// key: pattern,
// ** # end-of-line "double star comment"
// # own-line "double star comment"
// rest # end-of-line "after rest comment"
// # own-line "after rest comment"
// }
// ```
let (open_parenthesis_comments, double_star_comments, after_rest_comments) =
if let Some((double_star, rest)) = find_double_star(item, f.context().source()) {
let (open_parenthesis_comments, dangling) =
dangling.split_at(dangling.partition_point(|comment| {
comment.line_position().is_end_of_line()
&& comment.start() < double_star.start()
}));
let (double_star_comments, after_rest_comments) = dangling
.split_at(dangling.partition_point(|comment| comment.start() < rest.start()));
(
open_parenthesis_comments,
double_star_comments,
after_rest_comments,
)
} else {
(dangling, [].as_slice(), [].as_slice())
};
let format_pairs = format_with(|f| {
let mut joiner = f.join_comma_separated(item.end());
for (key, pattern) in keys.iter().zip(patterns) {
let key_pattern_pair = KeyPatternPair { key, pattern };
joiner.entry(&key_pattern_pair, &key_pattern_pair);
}
if let Some(identifier) = rest {
let rest_pattern = RestPattern {
identifier,
comments: double_star_comments,
};
joiner.entry(&rest_pattern, &rest_pattern);
}
joiner.finish()?;
trailing_comments(after_rest_comments).fmt(f)
});
parenthesized("{", &format_pairs, "}")
.with_dangling_comments(open_parenthesis_comments)
.fmt(f)
}
fn fmt_dangling_comments(
&self,
_dangling_comments: &[SourceComment],
_f: &mut PyFormatter,
) -> FormatResult<()> {
// Handled by `fmt_fields`
Ok(())
}
}
impl NeedsParentheses for PatternMatchMapping {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
}
}
/// A struct to format the `rest` element of a [`PatternMatchMapping`] (e.g., `{**rest}`).
#[derive(Debug)]
struct RestPattern<'a> {
identifier: &'a Identifier,
comments: &'a [SourceComment],
}
impl Ranged for RestPattern<'_> {
fn range(&self) -> TextRange {
self.identifier.range()
}
}
impl Format<PyFormatContext<'_>> for RestPattern<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
write!(
f,
[
leading_comments(self.comments),
text("**"),
self.identifier.format()
]
[not_yet_implemented_custom_text(
"{\"NOT_YET_IMPLEMENTED_PatternMatchMapping\": _, 2: _}",
item
)]
)
}
}
/// A struct to format a key-pattern pair of a [`PatternMatchMapping`] (e.g., `{key: pattern}`).
#[derive(Debug)]
struct KeyPatternPair<'a> {
key: &'a Expr,
pattern: &'a Pattern,
}
impl Ranged for KeyPatternPair<'_> {
fn range(&self) -> TextRange {
TextRange::new(self.key.start(), self.pattern.end())
}
}
impl Format<PyFormatContext<'_>> for KeyPatternPair<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
write!(
f,
[group(&format_args![
self.key.format(),
text(":"),
space(),
self.pattern.format()
])]
)
}
}
/// Given a [`PatternMatchMapping`], finds the range of the `**` element in the `rest` pattern,
/// if it exists.
fn find_double_star(pattern: &PatternMatchMapping, source: &str) -> Option<(TextRange, TextRange)> {
let PatternMatchMapping {
keys: _,
patterns,
rest,
range: _,
} = pattern;
// If there's no `rest` element, there's no `**`.
let Some(rest) = rest else {
return None;
};
let mut tokenizer =
SimpleTokenizer::starts_at(patterns.last().map_or(pattern.start(), Ranged::end), source);
let double_star = tokenizer.find(|token| token.kind() == SimpleTokenKind::DoubleStar)?;
Some((double_star.range(), rest.range()))
}

View File

@@ -1,9 +1,6 @@
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::PatternMatchOr;
use crate::context::PyFormatContext;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter};
#[derive(Default)]
@@ -20,13 +17,3 @@ impl FormatNodeRule<PatternMatchOr> for FormatPatternMatchOr {
)
}
}
impl NeedsParentheses for PatternMatchOr {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Multiline
}
}

View File

@@ -1,33 +1,37 @@
use ruff_formatter::prelude::format_with;
use ruff_formatter::{Format, FormatResult};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{PatternMatchSequence, Ranged};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::TextRange;
use ruff_python_ast::PatternMatchSequence;
use crate::builders::PyFormatterExtensions;
use crate::context::PyFormatContext;
use crate::expression::parentheses::{
empty_parenthesized, optional_parentheses, parenthesized, NeedsParentheses, OptionalParentheses,
};
use crate::expression::parentheses::{empty_parenthesized, optional_parentheses, parenthesized};
use crate::{FormatNodeRule, PyFormatter};
#[derive(Default)]
pub struct FormatPatternMatchSequence;
#[derive(Debug)]
enum SequenceType {
Tuple,
TupleNoParens,
List,
}
impl FormatNodeRule<PatternMatchSequence> for FormatPatternMatchSequence {
fn fmt_fields(&self, item: &PatternMatchSequence, f: &mut PyFormatter) -> FormatResult<()> {
let PatternMatchSequence { patterns, range } = item;
let sequence_type = match &f.context().source()[*range].chars().next() {
Some('(') => SequenceType::Tuple,
Some('[') => SequenceType::List,
_ => SequenceType::TupleNoParens,
};
let comments = f.context().comments().clone();
let dangling = comments.dangling(item);
let sequence_type = SequenceType::from_pattern(item, f.context().source());
if patterns.is_empty() {
return match sequence_type {
SequenceType::Tuple => empty_parenthesized("(", dangling, ")").fmt(f),
SequenceType::List => empty_parenthesized("[", dangling, "]").fmt(f),
SequenceType::Tuple | SequenceType::TupleNoParens => {
empty_parenthesized("(", dangling, ")").fmt(f)
SequenceType::TupleNoParens => {
unreachable!("If empty, it should be either tuple or list")
}
};
}
@@ -47,78 +51,3 @@ impl FormatNodeRule<PatternMatchSequence> for FormatPatternMatchSequence {
}
}
}
impl NeedsParentheses for PatternMatchSequence {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) enum SequenceType {
/// A list literal, e.g., `[1, 2, 3]`.
List,
/// A parenthesized tuple literal, e.g., `(1, 2, 3)`.
Tuple,
/// A tuple literal without parentheses, e.g., `1, 2, 3`.
TupleNoParens,
}
impl SequenceType {
pub(crate) fn from_pattern(pattern: &PatternMatchSequence, source: &str) -> SequenceType {
if source[pattern.range()].starts_with('[') {
SequenceType::List
} else if source[pattern.range()].starts_with('(') {
// If the pattern is empty, it must be a parenthesized tuple with no members. (This
// branch exists to differentiate between a tuple with and without its own parentheses,
// but a tuple without its own parentheses must have at least one member.)
let Some(elt) = pattern.patterns.first() else {
return SequenceType::Tuple;
};
// Count the number of open parentheses between the start of the pattern and the first
// element, and the number of close parentheses between the first element and its
// trailing comma. If the number of open parentheses is greater than the number of close
// parentheses,
// the pattern is parenthesized. For example, here, we have two parentheses before the
// first element, and one after it:
// ```python
// ((a), b, c)
// ```
//
// This algorithm successfully avoids false positives for cases like:
// ```python
// (a), b, c
// ```
let open_parentheses_count =
SimpleTokenizer::new(source, TextRange::new(pattern.start(), elt.start()))
.skip_trivia()
.filter(|token| token.kind() == SimpleTokenKind::LParen)
.count();
// Count the number of close parentheses.
let close_parentheses_count =
SimpleTokenizer::new(source, TextRange::new(elt.end(), elt.end()))
.skip_trivia()
.take_while(|token| token.kind() != SimpleTokenKind::Comma)
.filter(|token| token.kind() == SimpleTokenKind::RParen)
.count();
if open_parentheses_count > close_parentheses_count {
SequenceType::Tuple
} else {
SequenceType::TupleNoParens
}
} else {
SequenceType::TupleNoParens
}
}
pub(crate) fn is_parenthesized(self) -> bool {
matches!(self, SequenceType::List | SequenceType::Tuple)
}
}

View File

@@ -1,8 +1,6 @@
use crate::prelude::*;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{Constant, PatternMatchSingleton};
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::{FormatNodeRule, PyFormatter};
#[derive(Default)]
@@ -18,13 +16,3 @@ impl FormatNodeRule<PatternMatchSingleton> for FormatPatternMatchSingleton {
}
}
}
impl NeedsParentheses for PatternMatchSingleton {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
}
}

View File

@@ -1,10 +1,9 @@
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::node::AnyNodeRef;
use ruff_formatter::{prelude::text, write, Buffer, FormatResult};
use ruff_python_ast::PatternMatchStar;
use crate::comments::{dangling_comments, SourceComment};
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::prelude::*;
use crate::AsFormat;
use crate::{FormatNodeRule, PyFormatter};
#[derive(Default)]
pub struct FormatPatternMatchStar;
@@ -33,13 +32,3 @@ impl FormatNodeRule<PatternMatchStar> for FormatPatternMatchStar {
Ok(())
}
}
impl NeedsParentheses for PatternMatchStar {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
}
}

View File

@@ -1,26 +1,14 @@
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::PatternMatchValue;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, Parentheses};
use crate::prelude::*;
use crate::{AsFormat, FormatNodeRule, PyFormatter};
#[derive(Default)]
pub struct FormatPatternMatchValue;
impl FormatNodeRule<PatternMatchValue> for FormatPatternMatchValue {
fn fmt_fields(&self, item: &PatternMatchValue, f: &mut PyFormatter) -> FormatResult<()> {
// TODO(charlie): Avoid double parentheses for parenthesized top-level `PatternMatchValue`.
let PatternMatchValue { value, range: _ } = item;
value.format().with_options(Parentheses::Never).fmt(f)
}
}
impl NeedsParentheses for PatternMatchValue {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
value.format().fmt(f)
}
}

View File

@@ -304,7 +304,23 @@ long_unmergable_string_with_pragma = (
```diff
--- Black
+++ Ruff
@@ -165,13 +165,9 @@
@@ -143,9 +143,13 @@
)
)
-fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one."
+fstring = (
+ f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one."
+)
-fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever."
+fstring_with_no_fexprs = (
+ f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever."
+)
comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top.
@@ -165,13 +169,9 @@
triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched."""
@@ -320,7 +336,7 @@ long_unmergable_string_with_pragma = (
"formatting"
)
@@ -221,8 +217,8 @@
@@ -221,8 +221,8 @@
func_with_bad_comma(
(
"This is a really long string argument to a function that has a trailing comma"
@@ -331,6 +347,17 @@ long_unmergable_string_with_pragma = (
)
func_with_bad_parens_that_wont_fit_in_one_line(
@@ -274,7 +274,9 @@
yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three."
-x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four."
+x = (
+ f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four."
+)
long_unmergable_string_with_pragma = (
"This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore
```
## Ruff Output
@@ -481,9 +508,13 @@ old_fmt_string3 = (
)
)
fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one."
fstring = (
f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one."
)
fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever."
fstring_with_no_fexprs = (
f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever."
)
comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top.
@@ -608,7 +639,9 @@ def foo():
yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three."
x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four."
x = (
f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four."
)
long_unmergable_string_with_pragma = (
"This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore

View File

@@ -165,7 +165,7 @@ match x:
y = 0
# case black_test_patma_073
match x:
@@ -16,11 +16,11 @@
@@ -16,23 +16,23 @@
y = 1
# case black_test_patma_006
match 3:
@@ -179,9 +179,15 @@ match x:
y = 0
# case black_check_sequence_then_mapping
match x:
@@ -32,7 +32,7 @@
case [*_]:
return "seq"
- case {}:
+ case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
return "map"
# case black_test_patma_035
match x:
case {0: [1, 2, {}]}:
- case {0: [1, 2, {}]}:
+ case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
y = 0
- case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}:
+ case NOT_YET_IMPLEMENTED_PatternMatchOf | (y):
@@ -197,6 +203,30 @@ match x:
x = True
# case black_test_patma_154
match x:
@@ -54,11 +54,11 @@
y = 0
# case black_test_patma_134
match x:
- case {1: 0}:
+ case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
y = 0
- case {0: 0}:
+ case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
y = 1
- case {**z}:
+ case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
y = 2
# case black_test_patma_185
match Seq():
@@ -72,7 +72,7 @@
y = 1
# case black_test_patma_248
match x:
- case {"foo": bar}:
+ case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
y = bar
# case black_test_patma_019
match (0, 1, 2):
@@ -132,13 +132,13 @@
z = 0
# case black_test_patma_042
@@ -206,7 +236,8 @@ match x:
y = 0
# case black_test_patma_034
match x:
case {0: [1, 2, {}]}:
- case {0: [1, 2, {}]}:
+ case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
y = 0
- case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}:
+ case NOT_YET_IMPLEMENTED_PatternMatchOf | (y):
@@ -246,11 +277,11 @@ match x:
match x:
case [*_]:
return "seq"
case {}:
case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
return "map"
# case black_test_patma_035
match x:
case {0: [1, 2, {}]}:
case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
y = 0
case NOT_YET_IMPLEMENTED_PatternMatchOf | (y):
y = 1
@@ -274,11 +305,11 @@ match x:
y = 0
# case black_test_patma_134
match x:
case {1: 0}:
case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
y = 0
case {0: 0}:
case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
y = 1
case {**z}:
case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
y = 2
# case black_test_patma_185
match Seq():
@@ -292,7 +323,7 @@ match x:
y = 1
# case black_test_patma_248
match x:
case {"foo": bar}:
case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
y = bar
# case black_test_patma_019
match (0, 1, 2):
@@ -356,7 +387,7 @@ match x:
y = 0
# case black_test_patma_034
match x:
case {0: [1, 2, {}]}:
case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
y = 0
case NOT_YET_IMPLEMENTED_PatternMatchOf | (y):
y = 1

View File

@@ -188,6 +188,15 @@ match bar1:
pass
case _:
pass
@@ -48,7 +57,7 @@
match a, *b, c:
case [*_]:
assert "seq" == _
- case {}:
+ case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
assert "map" == b
@@ -59,12 +68,7 @@
),
case,
@@ -202,25 +211,27 @@ match bar1:
pass
case [a as match]:
@@ -87,10 +91,10 @@
@@ -85,12 +89,9 @@
match something:
case {
"key": key as key_1,
- case {
- "key": key as key_1,
- "password": PASS.ONE | PASS.TWO | PASS.THREE as password,
+ "password": NOT_YET_IMPLEMENTED_PatternMatchOf | (y) as password,
}:
- }:
+ case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
pass
- case {"maybe": something(complicated as this) as that}:
+ case {"maybe": NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0) as that}:
+ case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
pass
@@ -101,19 +105,17 @@
@@ -101,19 +102,17 @@
case 2 as b, 3 as c:
pass
- case 4 as d, (5 as e), (6 | 7 as g), *h:
+ case 4 as d, (5 as e), (NOT_YET_IMPLEMENTED_PatternMatchOf | (y) as g), *h:
+ case 4 as d, 5 as e, NOT_YET_IMPLEMENTED_PatternMatchOf | (y) as g, *h:
pass
@@ -302,7 +313,7 @@ match (
match a, *b, c:
case [*_]:
assert "seq" == _
case {}:
case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
assert "map" == b
@@ -334,12 +345,9 @@ match a, *b(), c:
match something:
case {
"key": key as key_1,
"password": NOT_YET_IMPLEMENTED_PatternMatchOf | (y) as password,
}:
case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
pass
case {"maybe": NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0) as that}:
case {"NOT_YET_IMPLEMENTED_PatternMatchMapping": _, 2: _}:
pass
@@ -350,7 +358,7 @@ match something:
case 2 as b, 3 as c:
pass
case 4 as d, (5 as e), (NOT_YET_IMPLEMENTED_PatternMatchOf | (y) as g), *h:
case 4 as d, 5 as e, NOT_YET_IMPLEMENTED_PatternMatchOf | (y) as g, *h:
pass

View File

@@ -117,13 +117,13 @@ def where_is(point):
match command.split():
- case ["go", ("north" | "south" | "east" | "west")]:
+ case ["go", (NOT_YET_IMPLEMENTED_PatternMatchOf | (y))]:
+ case ["go", NOT_YET_IMPLEMENTED_PatternMatchOf | (y)]:
current_room = current_room.neighbor(...)
# how do I know which direction to go?
match command.split():
- case ["go", ("north" | "south" | "east" | "west") as direction]:
+ case ["go", (NOT_YET_IMPLEMENTED_PatternMatchOf | (y)) as direction]:
+ case ["go", NOT_YET_IMPLEMENTED_PatternMatchOf | (y) as direction]:
current_room = current_room.neighbor(direction)
match command.split():
@@ -223,12 +223,12 @@ match command.split():
... # Code for picking up the given object
match command.split():
case ["go", (NOT_YET_IMPLEMENTED_PatternMatchOf | (y))]:
case ["go", NOT_YET_IMPLEMENTED_PatternMatchOf | (y)]:
current_room = current_room.neighbor(...)
# how do I know which direction to go?
match command.split():
case ["go", (NOT_YET_IMPLEMENTED_PatternMatchOf | (y)) as direction]:
case ["go", NOT_YET_IMPLEMENTED_PatternMatchOf | (y) as direction]:
current_room = current_room.neighbor(direction)
match command.split():

View File

@@ -0,0 +1,281 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_parens.py
---
## Input
```py
x = (1)
x = (1.2)
data = (
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
).encode()
async def show_status():
while True:
try:
if report_host:
data = (
f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
).encode()
except Exception as e:
pass
def example():
return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"))
def example1():
return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111))
def example1point5():
return ((((((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111))))))
def example2():
return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"))
def example3():
return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111))
def example4():
return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((True))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
def example5():
return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((()))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
def example6():
return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]})))))))))
def example7():
return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20000000000000000000]})))))))))
def example8():
return (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((None)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -11,8 +11,10 @@
try:
if report_host:
data = (
- f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- ).encode()
+ (
+ f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ ).encode()
+ )
except Exception as e:
pass
@@ -30,15 +32,11 @@
def example2():
- return (
- "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- )
+ return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
def example3():
- return (
- 1111111111111111111111111111111111111111111111111111111111111111111111111111111
- )
+ return 1111111111111111111111111111111111111111111111111111111111111111111111111111111
def example4():
```
## Ruff Output
```py
x = 1
x = 1.2
data = (
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
).encode()
async def show_status():
while True:
try:
if report_host:
data = (
(
f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
).encode()
)
except Exception as e:
pass
def example():
return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
def example1():
return 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111
def example1point5():
return 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111
def example2():
return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
def example3():
return 1111111111111111111111111111111111111111111111111111111111111111111111111111111
def example4():
return True
def example5():
return ()
def example6():
return {a: a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]}
def example7():
return {
a: a
for a in [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20000000000000000000,
]
}
def example8():
return None
```
## Black Output
```py
x = 1
x = 1.2
data = (
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
).encode()
async def show_status():
while True:
try:
if report_host:
data = (
f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
).encode()
except Exception as e:
pass
def example():
return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
def example1():
return 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111
def example1point5():
return 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111
def example2():
return (
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
)
def example3():
return (
1111111111111111111111111111111111111111111111111111111111111111111111111111111
)
def example4():
return True
def example5():
return ()
def example6():
return {a: a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]}
def example7():
return {
a: a
for a in [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20000000000000000000,
]
}
def example8():
return None
```

View File

@@ -56,18 +56,14 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx(
# Example from https://github.com/psf/black/issues/3229
@@ -43,8 +41,6 @@
)
@@ -45,6 +43,4 @@
# Regression test for https://github.com/psf/black/issues/3414.
-assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx(
- xxxxxxxxx
assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx(
xxxxxxxxx
-).xxxxxxxxxxxxxxxxxx(), (
- "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
-)
+assert (
+ xxxxxxxxx.xxxxxxxxx.xxxxxxxxx(xxxxxxxxx).xxxxxxxxxxxxxxxxxx()
+), "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+).xxxxxxxxxxxxxxxxxx(), "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```
## Ruff Output
@@ -116,9 +112,9 @@ assert (
)
# Regression test for https://github.com/psf/black/issues/3414.
assert (
xxxxxxxxx.xxxxxxxxx.xxxxxxxxx(xxxxxxxxx).xxxxxxxxxxxxxxxxxx()
), "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx(
xxxxxxxxx
).xxxxxxxxxxxxxxxxxx(), "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```
## Black Output

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