diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index 47b647677d096..aea50ed78f0bb 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -184,6 +184,7 @@ mod tests { use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; use crate::registry::Rule; + use crate::settings::options::LineWidth; use crate::settings::Settings; use super::check_physical_lines; @@ -196,7 +197,7 @@ mod tests { let indexer = Indexer::from_tokens(&tokens, &locator); let stylist = Stylist::from_tokens(&tokens, &locator); - let check_with_max_line_length = |line_length: usize| { + let check_with_max_line_length = |line_length: LineWidth| { check_physical_lines( Path::new("foo.py"), &locator, @@ -209,7 +210,8 @@ mod tests { }, ) }; - assert_eq!(check_with_max_line_length(8), vec![]); - assert_eq!(check_with_max_line_length(8), vec![]); + let line_length = LineWidth::from_line_length(8); + assert_eq!(check_with_max_line_length(line_length), vec![]); + assert_eq!(check_with_max_line_length(line_length), vec![]); } } diff --git a/crates/ruff/src/flake8_to_ruff/converter.rs b/crates/ruff/src/flake8_to_ruff/converter.rs index 8e54ae57bcfc7..14457e302d857 100644 --- a/crates/ruff/src/flake8_to_ruff/converter.rs +++ b/crates/ruff/src/flake8_to_ruff/converter.rs @@ -18,7 +18,7 @@ use crate::rules::{ flake8_annotations, flake8_bugbear, flake8_builtins, flake8_errmsg, flake8_pytest_style, flake8_quotes, flake8_tidy_imports, mccabe, pep8_naming, pydocstyle, }; -use crate::settings::options::Options; +use crate::settings::options::{Options, LineWidth}; use crate::settings::pyproject::Pyproject; use crate::settings::types::PythonVersion; use crate::warn_user; @@ -119,7 +119,7 @@ pub fn convert( options.builtins = Some(parser::parse_strings(value.as_ref())); } "max-line-length" | "max_line_length" => match value.parse::() { - Ok(line_length) => options.line_length = Some(line_length), + Ok(line_length) => options.line_length = Some(LineWidth::from_line_length(line_length)), Err(e) => { warn_user!("Unable to parse '{key}' property: {e}"); } @@ -402,7 +402,7 @@ pub fn convert( // Extract any settings from the existing `pyproject.toml`. if let Some(black) = &external_config.black { if let Some(line_length) = &black.line_length { - options.line_length = Some(*line_length); + options.line_length = Some(LineWidth::from_line_length(*line_length)); } if let Some(target_version) = &black.target_version { @@ -465,7 +465,7 @@ mod tests { use crate::rule_selector::RuleSelector; use crate::rules::pydocstyle::settings::Convention; use crate::rules::{flake8_quotes, pydocstyle}; - use crate::settings::options::Options; + use crate::settings::options::{Options, LineWidth}; use crate::settings::pyproject::Pyproject; use crate::settings::types::PythonVersion; @@ -508,7 +508,7 @@ mod tests { Some(vec![]), )?; let expected = Pyproject::new(Options { - line_length: Some(100), + line_length: Some(LineWidth::from_line_length(100)), ..default_options([]) }); assert_eq!(actual, expected); @@ -527,7 +527,7 @@ mod tests { Some(vec![]), )?; let expected = Pyproject::new(Options { - line_length: Some(100), + line_length: Some(LineWidth::from_line_length(100)), ..default_options([]) }); assert_eq!(actual, expected); diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs index 81c75f91c9ac9..6ef6c94829d29 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs @@ -15,7 +15,7 @@ use ruff_python_semantic::context::Context; use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::flake8_simplify::rules::fix_if; -use crate::rules::pycodestyle::helpers::WidthWithTabs; +use crate::settings::options::LineWidth; fn compare_expr(expr1: &ComparableExpr, expr2: &ComparableExpr) -> bool { expr1.eq(expr2) @@ -289,7 +289,7 @@ pub(crate) fn nested_if_statements( .unwrap_or_default() .universal_newlines() .all(|line| { - line.width_with_tabs(checker.settings.tab_size, None) + LineWidth::new(checker.settings.tab_size).add_str(&line) <= checker.settings.line_length }) { @@ -510,11 +510,11 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: O // Don't flag if the resulting expression would exceed the maximum line length. let line_start = checker.locator.line_start(stmt.start()); - let tab_size = checker.settings.tab_size; - let mut width = checker.locator.contents()[TextRange::new(line_start, stmt.start())] - .width_with_tabs(tab_size, None); - width = contents.width_with_tabs(tab_size, Some(width)); - if width > checker.settings.line_length { + if LineWidth::new(checker.settings.tab_size) + .add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())]) + .add_str(&contents) + > checker.settings.line_length + { return; } @@ -866,11 +866,11 @@ pub(crate) fn use_dict_get_with_default( // Don't flag if the resulting expression would exceed the maximum line length. let line_start = checker.locator.line_start(stmt.start()); - let tab_size = checker.settings.tab_size; - let mut width = checker.locator.contents()[TextRange::new(line_start, stmt.start())] - .width_with_tabs(tab_size, None); - width = contents.width_with_tabs(tab_size, Some(width)); - if width > checker.settings.line_length { + if LineWidth::new(checker.settings.tab_size) + .add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())]) + .add_str(&contents) + > checker.settings.line_length + { return; } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs index ac296e5f26948..ba108b158b0d5 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs @@ -10,7 +10,7 @@ use ruff_python_ast::newlines::StrExt; use crate::checkers::ast::Checker; use crate::registry::AsRule; -use crate::rules::pycodestyle::helpers::WidthWithTabs; +use crate::settings::options::LineWidth; use super::fix_with; @@ -112,7 +112,7 @@ pub(crate) fn multiple_with_statements( .unwrap_or_default() .universal_newlines() .all(|line| { - line.width_with_tabs(checker.settings.tab_size, None) + LineWidth::new(checker.settings.tab_size).add_str(&line) <= checker.settings.line_length }) { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs index 61fd2e03478df..6c7bea9981f15 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -10,7 +10,7 @@ use ruff_python_ast::source_code::Generator; use crate::checkers::ast::Checker; use crate::registry::{AsRule, Rule}; -use crate::rules::pycodestyle::helpers::WidthWithTabs; +use crate::settings::options::LineWidth; #[violation] pub struct ReimplementedBuiltin { @@ -225,12 +225,11 @@ pub(crate) fn convert_for_loop_to_any_all( // Don't flag if the resulting expression would exceed the maximum line length. let line_start = checker.locator.line_start(stmt.start()); - let tab_size = checker.settings.tab_size; - let mut width = checker.locator.contents() - [TextRange::new(line_start, stmt.start())] - .width_with_tabs(tab_size, None); - width = contents.width_with_tabs(tab_size, Some(width)); - if width > checker.settings.line_length { + if LineWidth::new(checker.settings.tab_size) + .add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())]) + .add_str(&contents) + > checker.settings.line_length + { return; } @@ -317,12 +316,11 @@ pub(crate) fn convert_for_loop_to_any_all( // Don't flag if the resulting expression would exceed the maximum line length. let line_start = checker.locator.line_start(stmt.start()); - let tab_size = checker.settings.tab_size; - let mut width = checker.locator.contents() - [TextRange::new(line_start, stmt.start())] - .width_with_tabs(tab_size, None); - width = contents.width_with_tabs(tab_size, Some(width)); - if width > checker.settings.line_length { + if LineWidth::new(checker.settings.tab_size) + .add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())]) + .add_str(&contents) + > checker.settings.line_length + { return; } diff --git a/crates/ruff/src/rules/isort/format.rs b/crates/ruff/src/rules/isort/format.rs index 316b7a381ad58..95dd6d1cb5aa8 100644 --- a/crates/ruff/src/rules/isort/format.rs +++ b/crates/ruff/src/rules/isort/format.rs @@ -1,8 +1,6 @@ use ruff_python_ast::source_code::Stylist; -use unicode_width::UnicodeWidthStr; -use crate::rules::pycodestyle::helpers::WidthWithTabs; -use crate::settings::options::TabSize; +use crate::settings::options::LineWidth; use super::types::{AliasData, CommentSet, ImportFromData, Importable}; @@ -47,9 +45,8 @@ pub(crate) fn format_import_from( import_from: &ImportFromData, comments: &CommentSet, aliases: &[(AliasData, CommentSet)], - line_length: usize, - indentation_width: usize, - tab_size: TabSize, + line_length: LineWidth, + indentation_width: LineWidth, stylist: &Stylist, force_wrap_aliases: bool, is_first: bool, @@ -66,7 +63,6 @@ pub(crate) fn format_import_from( aliases, is_first, stylist, - tab_size, indentation_width, ); return single_line; @@ -88,7 +84,6 @@ pub(crate) fn format_import_from( aliases, is_first, stylist, - tab_size, indentation_width, ); if import_width <= line_length || aliases.iter().any(|(alias, _)| alias.name == "*") { @@ -108,9 +103,8 @@ fn format_single_line( aliases: &[(AliasData, CommentSet)], is_first: bool, stylist: &Stylist, - tab_size: TabSize, - indentation_width: usize, -) -> (String, usize) { + indentation_width: LineWidth, +) -> (String, LineWidth) { let mut output = String::with_capacity(CAPACITY); let mut line_width = indentation_width; @@ -126,28 +120,28 @@ fn format_single_line( output.push_str("from "); output.push_str(&module_name); output.push_str(" import "); - line_width += 5 + module_name.width() + 8; + line_width = line_width.add_width(5).add_str(&module_name).add_width(8); for (index, (AliasData { name, asname }, comments)) in aliases.iter().enumerate() { if let Some(asname) = asname { output.push_str(name); output.push_str(" as "); output.push_str(asname); - line_width += name.width() + 4 + asname.width(); + line_width = line_width.add_str(name).add_width(4).add_str(asname); } else { output.push_str(name); - line_width += name.width(); + line_width = line_width.add_str(name); } if index < aliases.len() - 1 { output.push_str(", "); - line_width += 2; + line_width = line_width.add_width(2); } for comment in &comments.inline { output.push(' '); output.push(' '); output.push_str(comment); - line_width = comment.width_with_tabs(tab_size, Some(line_width + 2)); + line_width = line_width.add_width(2).add_str(comment); } } @@ -155,7 +149,7 @@ fn format_single_line( output.push(' '); output.push(' '); output.push_str(comment); - line_width = comment.width_with_tabs(tab_size, Some(line_width + 2)); + line_width = line_width.add_width(2).add_str(comment); } output.push_str(&stylist.line_ending()); diff --git a/crates/ruff/src/rules/isort/mod.rs b/crates/ruff/src/rules/isort/mod.rs index beb76ab9cf4ec..9df0d8317021b 100644 --- a/crates/ruff/src/rules/isort/mod.rs +++ b/crates/ruff/src/rules/isort/mod.rs @@ -18,7 +18,7 @@ use types::{AliasData, EitherImport, TrailingComma}; use crate::rules::isort::categorize::KnownModules; use crate::rules::isort::types::ImportBlock; -use crate::settings::options::TabSize; +use crate::settings::options::LineWidth; use crate::settings::types::PythonVersion; mod annotate; @@ -65,9 +65,8 @@ pub fn format_imports( block: &Block, comments: Vec, locator: &Locator, - line_length: usize, - indentation_width: usize, - tab_size: TabSize, + line_length: LineWidth, + indentation_width: LineWidth, stylist: &Stylist, src: &[PathBuf], package: Option<&Path>, @@ -110,7 +109,6 @@ pub fn format_imports( block, line_length, indentation_width, - tab_size, stylist, src, package, @@ -166,9 +164,8 @@ pub fn format_imports( #[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] fn format_import_block( block: ImportBlock, - line_length: usize, - indentation_width: usize, - tab_size: TabSize, + line_length: LineWidth, + indentation_width: LineWidth, stylist: &Stylist, src: &[PathBuf], package: Option<&Path>, @@ -271,7 +268,6 @@ fn format_import_block( &aliases, line_length, indentation_width, - tab_size, stylist, force_wrap_aliases, is_first_statement, diff --git a/crates/ruff/src/rules/isort/rules/organize_imports.rs b/crates/ruff/src/rules/isort/rules/organize_imports.rs index 02f2c51a21d0d..721eaf3a56a3a 100644 --- a/crates/ruff/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff/src/rules/isort/rules/organize_imports.rs @@ -14,8 +14,8 @@ use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; use ruff_python_ast::whitespace::leading_space; use crate::registry::AsRule; -use crate::rules::pycodestyle::helpers::WidthWithTabs; use crate::settings::Settings; +use crate::settings::options::LineWidth; use super::super::track::Block; use super::super::{comments, format_imports}; @@ -118,8 +118,7 @@ pub(crate) fn organize_imports( comments, locator, settings.line_length, - indentation.width_with_tabs(settings.tab_size, None), - settings.tab_size, + LineWidth::new(settings.tab_size).add_str(indentation), stylist, &settings.src, package, diff --git a/crates/ruff/src/rules/pycodestyle/helpers.rs b/crates/ruff/src/rules/pycodestyle/helpers.rs index 3682b1b02f797..42f5d8757e7d0 100644 --- a/crates/ruff/src/rules/pycodestyle/helpers.rs +++ b/crates/ruff/src/rules/pycodestyle/helpers.rs @@ -1,11 +1,11 @@ use ruff_text_size::{TextLen, TextRange}; use rustpython_parser::ast::{self, Cmpop, Expr}; -use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; +use unicode_width::UnicodeWidthStr; use ruff_python_ast::newlines::Line; use ruff_python_ast::source_code::Generator; -use crate::settings::options::TabSize; +use crate::settings::options::{LineWidth, TabSize}; pub(crate) fn is_ambiguous_name(name: &str) -> bool { name == "l" || name == "I" || name == "O" @@ -28,24 +28,19 @@ pub(crate) fn compare( pub(super) fn is_overlong( line: &Line, - limit: usize, + limit: LineWidth, ignore_overlong_task_comments: bool, task_tags: &[String], tab_size: TabSize, ) -> Option { - let tab_size: usize = tab_size.into(); let mut start_offset = line.start(); - let mut width = 0; + let mut width = LineWidth::new(tab_size); for c in line.chars() { if width < limit { start_offset += c.text_len(); } - width += if matches!(c, '\t') { - tab_size - (width % tab_size) - } else { - c.width().unwrap_or(0) - }; + width = width.add_char(c); } if width <= limit { @@ -71,14 +66,14 @@ pub(super) fn is_overlong( // begins before the limit. let last_chunk = chunks.last().unwrap_or(second_chunk); if last_chunk.contains("://") { - if width - last_chunk.width() <= limit { + if width.width() - last_chunk.width() <= limit.width() { return None; } } Some(Overlong { range: TextRange::new(start_offset, line.end()), - width, + width: width.width(), }) } @@ -96,22 +91,3 @@ impl Overlong { self.width } } - -pub(crate) trait WidthWithTabs { - fn width_with_tabs(&self, tab_size: TabSize, current_width: Option) -> usize; -} - -impl WidthWithTabs for str { - fn width_with_tabs(&self, tab_size: TabSize, current_width: Option) -> usize { - let tab_size: usize = tab_size.into(); - let current_width = current_width.unwrap_or(0); - self.chars().fold(current_width, |width, c| { - width - + if matches!(c, '\t') { - tab_size - (width % tab_size) - } else { - c.width().unwrap_or(0) - } - }) - } -} diff --git a/crates/ruff/src/rules/pycodestyle/mod.rs b/crates/ruff/src/rules/pycodestyle/mod.rs index a7ad599ceac0f..0e9073cbf23b1 100644 --- a/crates/ruff/src/rules/pycodestyle/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/mod.rs @@ -13,6 +13,7 @@ mod tests { use test_case::test_case; use crate::registry::Rule; + use crate::settings::options::LineWidth; use crate::test::test_path; use crate::{assert_messages, settings}; @@ -167,7 +168,7 @@ mod tests { Path::new("pycodestyle/W505.py"), &settings::Settings { pycodestyle: Settings { - max_doc_length: Some(50), + max_doc_length: Some(LineWidth::from_line_length(50)), ..Settings::default() }, ..settings::Settings::for_rule(Rule::DocLineTooLong) diff --git a/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs b/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs index 5845c35ab8e3b..b6715cb290be8 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs @@ -50,5 +50,5 @@ pub(crate) fn doc_line_too_long(line: &Line, settings: &Settings) -> Option Option, + pub max_doc_length: Option, #[option( default = "false", value_type = "bool", @@ -34,7 +36,7 @@ pub struct Options { #[derive(Debug, Default, CacheKey)] pub struct Settings { - pub max_doc_length: Option, + pub max_doc_length: Option, pub ignore_overlong_task_comments: bool, } diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__tab_size_4.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__tab_size_4.snap index badd15654682a..a3994ff6de854 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__tab_size_4.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__tab_size_4.snap @@ -8,11 +8,11 @@ E501_2.py:1:89: E501 Line too long (89 > 88 characters) 2 | a = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 | -E501_2.py:2:85: E501 Line too long (93 > 88 characters) +E501_2.py:2:87: E501 Line too long (91 > 88 characters) | 2 | a = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 3 | a = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 - | ^^^^^ E501 + | ^^^ E501 4 | 5 | b = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 | @@ -26,11 +26,11 @@ E501_2.py:4:89: E501 Line too long (89 > 88 characters) 7 | b = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 | -E501_2.py:5:85: E501 Line too long (93 > 88 characters) +E501_2.py:5:87: E501 Line too long (91 > 88 characters) | 5 | b = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 6 | b = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 - | ^^^^^ E501 + | ^^^ E501 7 | 8 | c = 901234567890123456789012345678901234567890123456789012345678901234567890123456789 | @@ -44,15 +44,6 @@ E501_2.py:7:89: E501 Line too long (89 > 88 characters) 10 | c = 901234567890123456789012345678901234567890123456789012345678901234567890123456789 | -E501_2.py:8:85: E501 Line too long (89 > 88 characters) - | - 8 | c = 901234567890123456789012345678901234567890123456789012345678901234567890123456789 - 9 | c = 901234567890123456789012345678901234567890123456789012345678901234567890123456789 - | ^ E501 -10 | -11 | d = 7890123456789012345678901234567890123456789012345678901234567890123456789 - | - E501_2.py:10:89: E501 Line too long (89 > 88 characters) | 10 | c = 901234567890123456789012345678901234567890123456789012345678901234567890123456789 diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__tab_size_8.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__tab_size_8.snap index 9078f8ef0a0c8..708c83ee20255 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__tab_size_8.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__tab_size_8.snap @@ -8,11 +8,11 @@ E501_2.py:1:89: E501 Line too long (89 > 88 characters) 2 | a = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 | -E501_2.py:2:77: E501 Line too long (101 > 88 characters) +E501_2.py:2:79: E501 Line too long (99 > 88 characters) | 2 | a = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 3 | a = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 - | ^^^^^^^^^^^^^ E501 + | ^^^^^^^^^^^ E501 4 | 5 | b = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 | @@ -26,11 +26,11 @@ E501_2.py:4:89: E501 Line too long (89 > 88 characters) 7 | b = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 | -E501_2.py:5:77: E501 Line too long (101 > 88 characters) +E501_2.py:5:79: E501 Line too long (99 > 88 characters) | 5 | b = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 6 | b = 5678901234567890123456789012345678901234567890123456789012345678901234567890123456789 - | ^^^^^^^^^^^^^ E501 + | ^^^^^^^^^^^ E501 7 | 8 | c = 901234567890123456789012345678901234567890123456789012345678901234567890123456789 | @@ -44,11 +44,11 @@ E501_2.py:7:89: E501 Line too long (89 > 88 characters) 10 | c = 901234567890123456789012345678901234567890123456789012345678901234567890123456789 | -E501_2.py:8:77: E501 Line too long (97 > 88 characters) +E501_2.py:8:79: E501 Line too long (95 > 88 characters) | 8 | c = 901234567890123456789012345678901234567890123456789012345678901234567890123456789 9 | c = 901234567890123456789012345678901234567890123456789012345678901234567890123456789 - | ^^^^^^^^^ E501 + | ^^^^^^^ E501 10 | 11 | d = 7890123456789012345678901234567890123456789012345678901234567890123456789 | @@ -62,11 +62,4 @@ E501_2.py:10:89: E501 Line too long (89 > 88 characters) 13 | d = 7890123456789012345678901234567890123456789012345678901234567890123456789 | -E501_2.py:11:77: E501 Line too long (89 > 88 characters) - | -11 | d = 7890123456789012345678901234567890123456789012345678901234567890123456789 -12 | d = 7890123456789012345678901234567890123456789012345678901234567890123456789 - | ^ E501 - | - diff --git a/crates/ruff/src/settings/configuration.rs b/crates/ruff/src/settings/configuration.rs index b8908500848f1..be272918fec2a 100644 --- a/crates/ruff/src/settings/configuration.rs +++ b/crates/ruff/src/settings/configuration.rs @@ -25,7 +25,7 @@ use crate::settings::types::{ FilePattern, PerFileIgnore, PythonVersion, SerializationFormat, Version, }; -use super::options::TabSize; +use super::options::{TabSize, LineWidth}; #[derive(Debug, Default)] pub struct RuleSelection { @@ -58,7 +58,7 @@ pub struct Configuration { pub format: Option, pub ignore_init_module_imports: Option, pub include: Option>, - pub line_length: Option, + pub line_length: Option, pub tab_size: Option, pub namespace_packages: Option>, pub required_version: Option, diff --git a/crates/ruff/src/settings/defaults.rs b/crates/ruff/src/settings/defaults.rs index 273ffc9e9d7bd..5be5d71719c24 100644 --- a/crates/ruff/src/settings/defaults.rs +++ b/crates/ruff/src/settings/defaults.rs @@ -16,7 +16,7 @@ use crate::rules::{ }; use crate::settings::types::FilePatternSet; -use super::options::TabSize; +use super::options::{LineWidth, TabSize}; use super::types::{FilePattern, PythonVersion}; use super::Settings; @@ -27,10 +27,6 @@ pub const PREFIXES: &[RuleSelector] = &[ pub const TARGET_VERSION: PythonVersion = PythonVersion::Py310; -pub const LINE_LENGTH: usize = 88; - -pub const TAB_SIZE: TabSize = TabSize(4); - pub const TASK_TAGS: &[&str] = &["TODO", "FIXME", "XXX"]; pub static DUMMY_VARIABLE_RGX: Lazy = @@ -79,8 +75,8 @@ impl Default for Settings { force_exclude: false, ignore_init_module_imports: false, include: FilePatternSet::try_from_vec(INCLUDE.clone()).unwrap(), - line_length: LINE_LENGTH, - tab_size: TAB_SIZE, + line_length: LineWidth::default(), + tab_size: TabSize::default(), namespace_packages: vec![], per_file_ignores: vec![], respect_gitignore: true, diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index 874e6526613a7..9cc2a930d6e33 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -25,7 +25,7 @@ use crate::settings::configuration::Configuration; use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion, SerializationFormat}; use crate::warn_user_once_by_id; -use self::options::TabSize; +use self::options::{TabSize, LineWidth}; use self::rule_table::RuleTable; pub mod configuration; @@ -99,7 +99,7 @@ pub struct Settings { pub dummy_variable_rgx: Regex, pub external: FxHashSet, pub ignore_init_module_imports: bool, - pub line_length: usize, + pub line_length: LineWidth, pub tab_size: TabSize, pub namespace_packages: Vec, pub src: Vec, @@ -162,8 +162,8 @@ impl Settings { config.include.unwrap_or_else(|| defaults::INCLUDE.clone()), )?, ignore_init_module_imports: config.ignore_init_module_imports.unwrap_or_default(), - line_length: config.line_length.unwrap_or(defaults::LINE_LENGTH), - tab_size: config.tab_size.unwrap_or(defaults::TAB_SIZE), + line_length: config.line_length.unwrap_or_default(), + tab_size: config.tab_size.unwrap_or_default(), namespace_packages: config.namespace_packages.unwrap_or_default(), per_file_ignores: resolve_per_file_ignores( config diff --git a/crates/ruff/src/settings/options.rs b/crates/ruff/src/settings/options.rs index 24f91e745fc68..84bdfbce78943 100644 --- a/crates/ruff/src/settings/options.rs +++ b/crates/ruff/src/settings/options.rs @@ -2,6 +2,7 @@ use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; +use unicode_width::UnicodeWidthChar; use ruff_macros::{CacheKey, ConfigurationOptions}; @@ -14,14 +15,120 @@ use crate::rules::{ }; use crate::settings::types::{PythonVersion, SerializationFormat, Version}; -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, CacheKey)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, CacheKey)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(from = "usize", into = "usize")] +pub struct LineWidth { + width: usize, + #[serde(skip)] + column: usize, + #[serde(skip)] + tab_size: TabSize, +} + +impl Default for LineWidth { + fn default() -> Self { + Self::from_line_length(88) + } +} + +impl LineWidth { + pub fn new(tab_size: TabSize) -> Self { + LineWidth { + width: 0, + column: 0, + tab_size, + } + } + + pub fn from_line_length(line_length: usize) -> Self { + Self { + width: line_length, + column: 0, + tab_size: TabSize::default(), + } + } + + pub const fn width(&self) -> usize { + self.width + } + fn update(mut self, chars: impl Iterator) -> Self { + let tab_size: usize = self.tab_size.into(); + for c in chars { + self.width += if matches!(c, '\t') { + tab_size - (self.column % tab_size) + } else { + c.width().unwrap_or(0) + }; + self.column += 1; + } + self + } + + #[must_use] + pub fn add_str(self, text: &str) -> Self { + self.update(text.chars()) + } + + #[must_use] + pub fn add_char(self, c: char) -> Self { + self.update(std::iter::once(c)) + } + + #[must_use] + pub fn add_width(mut self, width: usize) -> Self { + self.width += width; + self.column += width; + self + } +} + +impl PartialOrd for LineWidth { + fn partial_cmp(&self, other: &LineWidth) -> Option { + self.width.partial_cmp(&other.width) + } +} + +impl Ord for LineWidth { + fn cmp(&self, other: &LineWidth) -> std::cmp::Ordering { + self.width.cmp(&other.width) + } +} + +impl PartialEq for LineWidth { + fn eq(&self, other: &LineWidth) -> bool { + self.width == other.width + } +} + +impl Eq for LineWidth {} + +impl From for LineWidth { + fn from(width: usize) -> Self { + Self::from_line_length(width) + } +} + +impl From for usize { + fn from(line_width: LineWidth) -> usize { + line_width.width + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, CacheKey)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct TabSize(pub u8); +impl Default for TabSize { + fn default() -> Self { + Self(4) + } +} + impl From for TabSize { - fn from(n: u8) -> Self { - Self(n) + fn from(tab_size: u8) -> Self { + Self(tab_size) } } @@ -320,18 +427,18 @@ pub struct Options { default = "88", value_type = "int", example = r#" - # Allow lines to be as long as 120 characters. - line-length = 120 + # Allow lines to be as long as 120 characters. + line-length = 120 "# )] /// The line length to use when enforcing long-lines violations (like /// `E501`). - pub line_length: Option, + pub line_length: Option, #[option( default = "4", value_type = "int", example = r#" - tabulation-length = 8 + tab_size = 8 "# )] /// The tabulation size to calculate line length. diff --git a/crates/ruff/src/settings/pyproject.rs b/crates/ruff/src/settings/pyproject.rs index 929d5bb541235..dd444c98a60c1 100644 --- a/crates/ruff/src/settings/pyproject.rs +++ b/crates/ruff/src/settings/pyproject.rs @@ -158,6 +158,7 @@ mod tests { flake8_bugbear, flake8_builtins, flake8_errmsg, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_tidy_imports, mccabe, pep8_naming, }; + use crate::settings::options::LineWidth; use crate::settings::pyproject::{ find_settings_toml, parse_pyproject_toml, Options, Pyproject, Tools, }; @@ -200,7 +201,7 @@ line-length = 79 pyproject.tool, Some(Tools { ruff: Some(Options { - line_length: Some(79), + line_length: Some(LineWidth::from_line_length(79)), ..Options::default() }) }) @@ -301,7 +302,7 @@ other-attribute = 1 config, Options { allowed_confusables: Some(vec!['−', 'ρ', '∗']), - line_length: Some(88), + line_length: Some(LineWidth::from_line_length(88)), extend_exclude: Some(vec![ "excluded_file.py".to_string(), "migrations".to_string(), diff --git a/crates/ruff_cli/src/args.rs b/crates/ruff_cli/src/args.rs index b8548c33c9818..55c9d52c99101 100644 --- a/crates/ruff_cli/src/args.rs +++ b/crates/ruff_cli/src/args.rs @@ -7,6 +7,7 @@ use ruff::logging::LogLevel; use ruff::registry::Rule; use ruff::resolver::ConfigProcessor; use ruff::settings::configuration::RuleSelection; +use ruff::settings::options::LineWidth; use ruff::settings::types::{ FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion, SerializationFormat, }; @@ -545,7 +546,7 @@ impl ConfigProcessor for &Overrides { config.force_exclude = Some(*force_exclude); } if let Some(line_length) = &self.line_length { - config.line_length = Some(*line_length); + config.line_length = Some(LineWidth::from_line_length(*line_length)); } if let Some(per_file_ignores) = &self.per_file_ignores { config.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone())); diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index d0d3064da5503..846c099323d33 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -14,7 +14,7 @@ use ruff::rules::{ flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint, }; use ruff::settings::configuration::Configuration; -use ruff::settings::options::Options; +use ruff::settings::options::{Options, LineWidth, TabSize}; use ruff::settings::{defaults, flags, Settings}; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Indexer, Locator, SourceLocation, Stylist}; @@ -102,8 +102,8 @@ pub fn defaultSettings() -> Result { extend_unfixable: Some(Vec::default()), external: Some(Vec::default()), ignore: Some(Vec::default()), - line_length: Some(defaults::LINE_LENGTH), - tab_size: Some(defaults::TAB_SIZE), + line_length: Some(LineWidth::default()), + tab_size: Some(TabSize::default()), select: Some(defaults::PREFIXES.to_vec()), target_version: Some(defaults::TARGET_VERSION), // Ignore a bunch of options that don't make sense in a single-file editor. diff --git a/ruff.schema.json b/ruff.schema.json index bed4ac1c7f28d..17fbccf48f808 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -366,12 +366,14 @@ }, "line-length": { "description": "The line length to use when enforcing long-lines violations (like `E501`).", - "type": [ - "integer", - "null" - ], - "format": "uint", - "minimum": 0.0 + "anyOf": [ + { + "$ref": "#/definitions/LineWidth" + }, + { + "type": "null" + } + ] }, "mccabe": { "description": "Options for the `mccabe` plugin.", @@ -505,12 +507,14 @@ }, "tab-size": { "description": "The tabulation size to calculate line length.", - "type": [ - "integer", - "null" - ], - "format": "uint8", - "minimum": 0.0 + "anyOf": [ + { + "$ref": "#/definitions/TabSize" + }, + { + "type": "null" + } + ] }, "target-version": { "description": "The Python version to target, e.g., when considering automatic code upgrades, like rewriting type annotations.\n\nIf omitted, the target version will be inferred from the `project.requires-python` field in the relevant `pyproject.toml` (e.g., `requires-python = \">=3.8\"`), if present.", @@ -1285,6 +1289,19 @@ }, "additionalProperties": false }, + "LineWidth": { + "type": "object", + "required": [ + "width" + ], + "properties": { + "width": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + } + } + }, "McCabeOptions": { "type": "object", "properties": { @@ -1370,12 +1387,14 @@ }, "max-doc-length": { "description": "The maximum line length to allow for line-length violations within documentation (`W505`), including standalone comments.", - "type": [ - "integer", - "null" - ], - "format": "uint", - "minimum": 0.0 + "anyOf": [ + { + "$ref": "#/definitions/LineWidth" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -2498,6 +2517,11 @@ } ] }, + "TabSize": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, "Version": { "type": "string" }