diff --git a/src/checkers/imports.rs b/src/checkers/imports.rs index 3c7e7e3c57..8004f1d6fb 100644 --- a/src/checkers/imports.rs +++ b/src/checkers/imports.rs @@ -40,7 +40,7 @@ pub fn check_imports( for block in &blocks { if !block.imports.is_empty() { if let Some(diagnostic) = isort::rules::organize_imports( - block, locator, indexer, settings, stylist, autofix, package, + block, locator, stylist, indexer, settings, autofix, package, ) { diagnostics.push(diagnostic); } @@ -49,7 +49,7 @@ pub fn check_imports( } if settings.rules.enabled(&Rule::MissingRequiredImport) { diagnostics.extend(isort::rules::add_required_imports( - &blocks, python_ast, locator, settings, autofix, + &blocks, python_ast, locator, stylist, settings, autofix, )); } diagnostics diff --git a/src/checkers/lines.rs b/src/checkers/lines.rs index 4c3efd2aeb..72a1eef03e 100644 --- a/src/checkers/lines.rs +++ b/src/checkers/lines.rs @@ -13,9 +13,11 @@ use crate::rules::pycodestyle::rules::{ use crate::rules::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore}; use crate::rules::pyupgrade::rules::unnecessary_coding_comment; use crate::settings::{flags, Settings}; +use crate::source_code::Stylist; pub fn check_lines( path: &Path, + stylist: &Stylist, contents: &str, commented_lines: &[usize], doc_lines: &[usize], @@ -139,6 +141,7 @@ pub fn check_lines( if enforce_no_newline_at_end_of_file { if let Some(diagnostic) = no_newline_at_end_of_file( + stylist, contents, matches!(autofix, flags::Autofix::Enabled) && settings.rules.should_fix(&Rule::NoNewLineAtEndOfFile), @@ -164,13 +167,18 @@ mod tests { use super::check_lines; use crate::registry::Rule; use crate::settings::{flags, Settings}; + use crate::source_code::{Locator, Stylist}; #[test] fn e501_non_ascii_char() { let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8. + let locator = Locator::new(line); + let stylist = Stylist::from_contents(line, &locator); + let check_with_max_line_length = |line_length: usize| { check_lines( Path::new("foo.py"), + &stylist, line, &[], &[], diff --git a/src/linter.rs b/src/linter.rs index f0a85fe098..073bc039f2 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -143,6 +143,7 @@ pub fn check_path( { diagnostics.extend(check_lines( path, + stylist, contents, indexer.commented_lines(), &doc_lines, diff --git a/src/rules/flake8_return/rules.rs b/src/rules/flake8_return/rules.rs index f9e289a06e..a07ac37b0e 100644 --- a/src/rules/flake8_return/rules.rs +++ b/src/rules/flake8_return/rules.rs @@ -111,7 +111,7 @@ fn implicit_return(checker: &mut Checker, last_stmt: &Stmt) { let mut content = String::new(); content.push_str(indent); content.push_str("return None"); - content.push('\n'); + content.push_str(checker.stylist.line_ending().as_str()); diagnostic.amend(Fix::insertion( content, Location::new(last_stmt.end_location.unwrap().row() + 1, 0), diff --git a/src/rules/flake8_simplify/rules/ast_with.rs b/src/rules/flake8_simplify/rules/ast_with.rs index 13b668c4da..79f876a1c7 100644 --- a/src/rules/flake8_simplify/rules/ast_with.rs +++ b/src/rules/flake8_simplify/rules/ast_with.rs @@ -55,7 +55,11 @@ pub fn multiple_with_statements( Range::new(with_stmt.location, nested_with.location), checker.locator, ) { - match fix_with::fix_multiple_with_statements(checker.locator, with_stmt) { + match fix_with::fix_multiple_with_statements( + checker.locator, + checker.stylist, + with_stmt, + ) { Ok(fix) => { if fix .content diff --git a/src/rules/flake8_simplify/rules/fix_with.rs b/src/rules/flake8_simplify/rules/fix_with.rs index 888b26f4c4..a947cacf27 100644 --- a/src/rules/flake8_simplify/rules/fix_with.rs +++ b/src/rules/flake8_simplify/rules/fix_with.rs @@ -6,11 +6,12 @@ use crate::ast::types::Range; use crate::ast::whitespace; use crate::cst::matchers::match_module; use crate::fix::Fix; -use crate::source_code::Locator; +use crate::source_code::{Locator, Stylist}; /// (SIM117) Convert `with a: with b:` to `with a, b:`. pub(crate) fn fix_multiple_with_statements( locator: &Locator, + stylist: &Stylist, stmt: &rustpython_ast::Stmt, ) -> Result { // Infer the indentation of the outer block. @@ -30,7 +31,7 @@ pub(crate) fn fix_multiple_with_statements( let module_text = if outer_indent.is_empty() { contents.to_string() } else { - format!("def f():\n{contents}") + format!("def f():{}{contents}", stylist.line_ending().as_str()) }; // Parse the CST. @@ -75,7 +76,10 @@ pub(crate) fn fix_multiple_with_statements( } outer_with.body = inner_with.body.clone(); - let mut state = CodegenState::default(); + let mut state = CodegenState { + default_newline: stylist.line_ending(), + ..Default::default() + }; tree.codegen(&mut state); // Reconstruct and reformat the code. @@ -83,7 +87,10 @@ pub(crate) fn fix_multiple_with_statements( let contents = if outer_indent.is_empty() { module_text } else { - module_text.strip_prefix("def f():\n").unwrap().to_string() + module_text + .strip_prefix(&format!("def f():{}", stylist.line_ending().as_str())) + .unwrap() + .to_string() }; Ok(Fix::replacement( diff --git a/src/rules/isort/mod.rs b/src/rules/isort/mod.rs index d5aa0573a3..bca6b7f6e1 100644 --- a/src/rules/isort/mod.rs +++ b/src/rules/isort/mod.rs @@ -701,8 +701,6 @@ mod tests { #[test_case(Path::new("insert_empty_lines.py"))] #[test_case(Path::new("insert_empty_lines.pyi"))] #[test_case(Path::new("leading_prefix.py"))] - #[test_case(Path::new("line_ending_crlf.py"))] - #[test_case(Path::new("line_ending_lf.py"))] #[test_case(Path::new("magic_trailing_comma.py"))] #[test_case(Path::new("natural_order.py"))] #[test_case(Path::new("no_reorder_within_section.py"))] @@ -743,6 +741,25 @@ mod tests { Ok(()) } + // Test currently disabled as line endings are automatically converted to + // platform-appropriate ones in CI/CD #[test_case(Path::new(" + // line_ending_crlf.py"))] #[test_case(Path::new("line_ending_lf.py"))] + // fn source_code_style(path: &Path) -> Result<()> { + // let snapshot = format!("{}", path.to_string_lossy()); + // let diagnostics = test_path( + // Path::new("./resources/test/fixtures/isort") + // .join(path) + // .as_path(), + // &Settings { + // src: + // vec![Path::new("resources/test/fixtures/isort").to_path_buf()], + // ..Settings::for_rule(Rule::UnsortedImports) + // }, + // )?; + // insta::assert_yaml_snapshot!(snapshot, diagnostics); + // Ok(()) + // } + #[test_case(Path::new("combine_as_imports.py"))] fn combine_as_imports(path: &Path) -> Result<()> { let snapshot = format!("combine_as_imports_{}", path.to_string_lossy()); diff --git a/src/rules/isort/rules/add_required_imports.rs b/src/rules/isort/rules/add_required_imports.rs index c445bcfa88..67d37bc987 100644 --- a/src/rules/isort/rules/add_required_imports.rs +++ b/src/rules/isort/rules/add_required_imports.rs @@ -10,7 +10,7 @@ use crate::ast::types::Range; use crate::fix::Fix; use crate::registry::{Diagnostic, Rule}; use crate::settings::{flags, Settings}; -use crate::source_code::Locator; +use crate::source_code::{Locator, Stylist}; use crate::violations; struct Alias<'a> { @@ -102,6 +102,7 @@ fn add_required_import( blocks: &[&Block], python_ast: &Suite, locator: &Locator, + stylist: &Stylist, settings: &Settings, autofix: flags::Autofix, ) -> Option { @@ -134,19 +135,22 @@ fn add_required_import( // Generate the edit. let mut contents = String::with_capacity(required_import.len() + 1); + // Newline (LF/CRLF) + let line_sep = stylist.line_ending().as_str(); + // If we're inserting beyond the start of the file, we add // a newline _before_, since the splice represents the _end_ of the last // irrelevant token (e.g., the end of a comment or the end of // docstring). This ensures that we properly handle awkward cases like // docstrings that are followed by semicolons. if splice > Location::default() { - contents.push('\n'); + contents.push_str(line_sep); } contents.push_str(&required_import); // If we're inserting at the start of the file, add a trailing newline instead. if splice == Location::default() { - contents.push('\n'); + contents.push_str(line_sep); } // Construct the fix. @@ -160,6 +164,7 @@ pub fn add_required_imports( blocks: &[&Block], python_ast: &Suite, locator: &Locator, + stylist: &Stylist, settings: &Settings, autofix: flags::Autofix, ) -> Vec { @@ -192,6 +197,7 @@ pub fn add_required_imports( blocks, python_ast, locator, + stylist, settings, autofix, ) @@ -208,7 +214,8 @@ pub fn add_required_imports( }), blocks, python_ast, - locator, + locator, + stylist, settings, autofix, ) diff --git a/src/rules/isort/rules/organize_imports.rs b/src/rules/isort/rules/organize_imports.rs index eb55dd662e..8b8d8ab76f 100644 --- a/src/rules/isort/rules/organize_imports.rs +++ b/src/rules/isort/rules/organize_imports.rs @@ -31,9 +31,9 @@ fn extract_indentation_range(body: &[&Stmt]) -> Range { pub fn organize_imports( block: &Block, locator: &Locator, + stylist: &Stylist, indexer: &Indexer, settings: &Settings, - stylist: &Stylist, autofix: flags::Autofix, package: Option<&Path>, ) -> Option { diff --git a/src/rules/pycodestyle/rules/do_not_assign_lambda.rs b/src/rules/pycodestyle/rules/do_not_assign_lambda.rs index 16a39d1298..89c43e3c4b 100644 --- a/src/rules/pycodestyle/rules/do_not_assign_lambda.rs +++ b/src/rules/pycodestyle/rules/do_not_assign_lambda.rs @@ -35,7 +35,7 @@ pub fn do_not_assign_lambda(checker: &mut Checker, target: &Expr, value: &Expr, if idx == 0 { indented.push_str(line); } else { - indented.push('\n'); + indented.push_str(checker.stylist.line_ending().as_str()); indented.push_str(indentation); indented.push_str(line); } diff --git a/src/rules/pycodestyle/rules/no_newline_at_end_of_file.rs b/src/rules/pycodestyle/rules/no_newline_at_end_of_file.rs index fc5693a1cf..8baaf9896c 100644 --- a/src/rules/pycodestyle/rules/no_newline_at_end_of_file.rs +++ b/src/rules/pycodestyle/rules/no_newline_at_end_of_file.rs @@ -3,10 +3,15 @@ use rustpython_ast::Location; use crate::ast::types::Range; use crate::fix::Fix; use crate::registry::Diagnostic; +use crate::source_code::Stylist; use crate::violations; /// W292 -pub fn no_newline_at_end_of_file(contents: &str, autofix: bool) -> Option { +pub fn no_newline_at_end_of_file( + stylist: &Stylist, + contents: &str, + autofix: bool, +) -> Option { if !contents.ends_with('\n') { // Note: if `lines.last()` is `None`, then `contents` is empty (and so we don't // want to raise W292 anyway). @@ -18,7 +23,7 @@ pub fn no_newline_at_end_of_file(contents: &str, autofix: bool) -> Option = Lazy::new(|| Regex::new(r"\\[^\nuN]").unwrap()); +pub static BACKSLASH_REGEX: Lazy = Lazy::new(|| Regex::new(r"\\[^(\r\n|\n)uN]").unwrap()); + pub static COMMENT_REGEX: Lazy = Lazy::new(|| Regex::new(r"^\s*#").unwrap()); pub static INNER_FUNCTION_OR_CLASS_REGEX: Lazy = Lazy::new(|| Regex::new(r"^\s+(?:(?:class|def|async def)\s|@)").unwrap()); diff --git a/src/rules/pydocstyle/rules/sections.rs b/src/rules/pydocstyle/rules/sections.rs index b47e15e7b6..8d129d9f21 100644 --- a/src/rules/pydocstyle/rules/sections.rs +++ b/src/rules/pydocstyle/rules/sections.rs @@ -84,9 +84,10 @@ fn blanks_and_section_underline( if checker.patch(diagnostic.kind.rule()) { // Add a dashed line (of the appropriate length) under the section header. let content = format!( - "{}{}\n", + "{}{}{}", whitespace::clean(docstring.indentation), - "-".repeat(context.section_name.len()) + "-".repeat(context.section_name.len()), + checker.stylist.line_ending().as_str() ); diagnostic.amend(Fix::insertion( content, @@ -164,9 +165,10 @@ fn blanks_and_section_underline( if checker.patch(diagnostic.kind.rule()) { // Replace the existing underline with a line of the appropriate length. let content = format!( - "{}{}\n", + "{}{}{}", whitespace::clean(docstring.indentation), - "-".repeat(context.section_name.len()) + "-".repeat(context.section_name.len()), + checker.stylist.line_ending().as_str() ); diagnostic.amend(Fix::replacement( content, @@ -300,9 +302,10 @@ fn blanks_and_section_underline( if checker.patch(diagnostic.kind.rule()) { // Add a dashed line (of the appropriate length) under the section header. let content = format!( - "{}{}\n", + "{}{}{}", whitespace::clean(docstring.indentation), - "-".repeat(context.section_name.len()) + "-".repeat(context.section_name.len()), + checker.stylist.line_ending().as_str() ); diagnostic.amend(Fix::insertion( content, @@ -416,6 +419,7 @@ fn common_section( } } + let line_end = checker.stylist.line_ending().as_str(); if context .following_lines .last() @@ -434,7 +438,7 @@ fn common_section( if checker.patch(diagnostic.kind.rule()) { // Add a newline after the section. diagnostic.amend(Fix::insertion( - "\n".to_string(), + line_end.to_string(), Location::new( docstring.expr.location.row() + context.original_index @@ -455,7 +459,7 @@ fn common_section( if checker.patch(diagnostic.kind.rule()) { // Add a newline after the section. diagnostic.amend(Fix::insertion( - "\n".to_string(), + line_end.to_string(), Location::new( docstring.expr.location.row() + context.original_index @@ -483,7 +487,7 @@ fn common_section( if checker.patch(diagnostic.kind.rule()) { // Add a blank line before the section. diagnostic.amend(Fix::insertion( - "\n".to_string(), + line_end.to_string(), Location::new(docstring.expr.location.row() + context.original_index, 0), )); } @@ -568,7 +572,7 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: & // See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`. static GOOGLE_ARGS_REGEX: Lazy = - Lazy::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:\n?\s*.+").unwrap()); + Lazy::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap()); fn args_section(checker: &mut Checker, docstring: &Docstring, context: &SectionContext) { if context.following_lines.is_empty() { diff --git a/src/rules/pyupgrade/rules/printf_string_formatting.rs b/src/rules/pyupgrade/rules/printf_string_formatting.rs index f9a91a839d..a98dd3eaa4 100644 --- a/src/rules/pyupgrade/rules/printf_string_formatting.rs +++ b/src/rules/pyupgrade/rules/printf_string_formatting.rs @@ -210,13 +210,13 @@ fn clean_params_dictionary(checker: &mut Checker, right: &Expr) -> Option { fn dirty_count(iter: impl Iterator) -> usize { let mut the_count = 0; for current_char in iter { - if current_char == ' ' || current_char == ',' || current_char == '\n' { + if current_char == ' ' + || current_char == ',' + || current_char == '\n' + || current_char == '\r' + { the_count += 1; } else { break; @@ -43,7 +47,13 @@ fn extract_middle(contents: &str) -> Option { } /// Generate a [`Fix`] for a `stdout` and `stderr` [`Keyword`] pair. -fn generate_fix(locator: &Locator, stdout: &Keyword, stderr: &Keyword) -> Option { +fn generate_fix( + stylist: &Stylist, + locator: &Locator, + stdout: &Keyword, + stderr: &Keyword, +) -> Option { + let line_end = stylist.line_ending().as_str(); let first = if stdout.location < stderr.location { stdout } else { @@ -63,7 +73,7 @@ fn generate_fix(locator: &Locator, stdout: &Keyword, stderr: &Keyword) -> Option return None; }; contents.push(','); - contents.push('\n'); + contents.push_str(line_end); contents.push_str(indent); } else { contents.push(','); @@ -109,7 +119,7 @@ pub fn replace_stdout_stderr(checker: &mut Checker, expr: &Expr, kwargs: &[Keywo let mut diagnostic = Diagnostic::new(violations::ReplaceStdoutStderr, Range::from_located(expr)); if checker.patch(diagnostic.kind.rule()) { - if let Some(fix) = generate_fix(checker.locator, stdout, stderr) { + if let Some(fix) = generate_fix(checker.stylist, checker.locator, stdout, stderr) { diagnostic.amend(fix); }; } diff --git a/src/source_code/generator.rs b/src/source_code/generator.rs index 87dffb36eb..0502978812 100644 --- a/src/source_code/generator.rs +++ b/src/source_code/generator.rs @@ -1104,7 +1104,10 @@ mod tests { macro_rules! assert_round_trip { ($contents:expr) => { - assert_eq!(round_trip($contents), $contents); + assert_eq!( + round_trip($contents), + $contents.replace('\n', LineEnding::default().as_str()) + ); }; } @@ -1193,6 +1196,7 @@ if True: pass "# .trim() + .replace('\n', LineEnding::default().as_str()) ); } @@ -1254,6 +1258,7 @@ if True: pass "# .trim() + .replace('\n', LineEnding::default().as_str()) ); assert_eq!( round_trip_with( @@ -1271,6 +1276,7 @@ if True: pass "# .trim() + .replace('\n', LineEnding::default().as_str()) ); assert_eq!( round_trip_with( @@ -1288,6 +1294,7 @@ if True: pass "# .trim() + .replace('\n', LineEnding::default().as_str()) ); }