diff --git a/README.md b/README.md index f77c4d9f23..978a241654 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com | E743 | AmbiguousFunctionName | Ambiguous function name: `...` | ✅ | | | E902 | IOError | IOError: `...` | ✅ | | | E999 | SyntaxError | SyntaxError: `...` | ✅ | | +| W292 | NoNewLineAtEndOfFile | No newline at end of file | ✅ | | | F401 | UnusedImport | `...` imported but unused | ✅ | 🛠 | | F402 | ImportShadowedByLoopVar | Import `...` from line 1 shadowed by loop variable | ✅ | | | F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names | ✅ | | @@ -291,6 +292,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com | U005 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead | | 🛠 | | M001 | UnusedNOQA | Unused `noqa` directive | | 🛠 | + ## Integrations ### PyCharm diff --git a/resources/test/fixtures/W292_0.py b/resources/test/fixtures/W292_0.py new file mode 100644 index 0000000000..fcb9a4b357 --- /dev/null +++ b/resources/test/fixtures/W292_0.py @@ -0,0 +1,2 @@ +def fn() -> None: + pass \ No newline at end of file diff --git a/resources/test/fixtures/W292_1.py b/resources/test/fixtures/W292_1.py new file mode 100644 index 0000000000..5a5428b92f --- /dev/null +++ b/resources/test/fixtures/W292_1.py @@ -0,0 +1,2 @@ +def fn() -> None: + pass # noqa: W292 \ No newline at end of file diff --git a/src/check_lines.rs b/src/check_lines.rs index 3800836b23..f3b30bebe8 100644 --- a/src/check_lines.rs +++ b/src/check_lines.rs @@ -113,6 +113,46 @@ pub fn check_lines( } } + // Enforce newlines at end of files. + if settings.enabled.contains(&CheckCode::W292) { + // If the file terminates with a newline, the last line should be an empty string slice. + if let Some(line) = lines.last() { + if !line.is_empty() { + let lineno = lines.len() - 1; + let noqa_lineno = noqa_line_for + .get(lineno) + .map(|lineno| lineno - 1) + .unwrap_or(lineno); + + let noqa = noqa_directives + .entry(noqa_lineno) + .or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![])); + + let check = Check::new( + CheckKind::NoNewLineAtEndOfFile, + Range { + location: Location::new(lines.len(), line.len() + 1), + end_location: Location::new(lines.len(), line.len() + 1), + }, + ); + + match noqa { + (Directive::All(_, _), matches) => { + matches.push(check.kind.code().as_str()); + } + (Directive::Codes(_, _, codes), matches) => { + if codes.contains(&check.kind.code().as_str()) { + matches.push(check.kind.code().as_str()); + } else { + line_checks.push(check); + } + } + (Directive::None, _) => line_checks.push(check), + } + } + } + } + // Enforce that the noqa directive was actually used. if enforce_noqa { for (row, (directive, matches)) in noqa_directives { diff --git a/src/checks.rs b/src/checks.rs index f683500e5d..d5fc518571 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize}; use crate::ast::types::Range; -pub const DEFAULT_CHECK_CODES: [CheckCode; 42] = [ - // pycodestyle +pub const DEFAULT_CHECK_CODES: [CheckCode; 43] = [ + // pycodestyle errors CheckCode::E402, CheckCode::E501, CheckCode::E711, @@ -24,6 +24,8 @@ pub const DEFAULT_CHECK_CODES: [CheckCode; 42] = [ CheckCode::E743, CheckCode::E902, CheckCode::E999, + // pycodestyle warnings + CheckCode::W292, // pyflakes CheckCode::F401, CheckCode::F402, @@ -55,8 +57,8 @@ pub const DEFAULT_CHECK_CODES: [CheckCode; 42] = [ CheckCode::F901, ]; -pub const ALL_CHECK_CODES: [CheckCode; 58] = [ - // pycodestyle +pub const ALL_CHECK_CODES: [CheckCode; 59] = [ + // pycodestyle errors CheckCode::E402, CheckCode::E501, CheckCode::E711, @@ -71,6 +73,8 @@ pub const ALL_CHECK_CODES: [CheckCode; 58] = [ CheckCode::E743, CheckCode::E902, CheckCode::E999, + // pycodestyle warnings + CheckCode::W292, // pyflakes CheckCode::F401, CheckCode::F402, @@ -126,7 +130,7 @@ pub const ALL_CHECK_CODES: [CheckCode; 58] = [ #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Hash, PartialOrd, Ord)] pub enum CheckCode { - // pycodestyle + // pycodestyle errors E402, E501, E711, @@ -141,6 +145,8 @@ pub enum CheckCode { E743, E902, E999, + // pycodestyle warnings + W292, // pyflakes F401, F402, @@ -199,7 +205,7 @@ impl FromStr for CheckCode { fn from_str(s: &str) -> Result { match s { - // pycodestyle + // pycodestyle errors "E402" => Ok(CheckCode::E402), "E501" => Ok(CheckCode::E501), "E711" => Ok(CheckCode::E711), @@ -214,6 +220,8 @@ impl FromStr for CheckCode { "E743" => Ok(CheckCode::E743), "E902" => Ok(CheckCode::E902), "E999" => Ok(CheckCode::E999), + // pycodestyle warnings + "W292" => Ok(CheckCode::W292), // pyflakes "F401" => Ok(CheckCode::F401), "F402" => Ok(CheckCode::F402), @@ -271,7 +279,7 @@ impl FromStr for CheckCode { impl CheckCode { pub fn as_str(&self) -> &str { match self { - // pycodestyle + // pycodestyle errors CheckCode::E402 => "E402", CheckCode::E501 => "E501", CheckCode::E711 => "E711", @@ -286,6 +294,8 @@ impl CheckCode { CheckCode::E743 => "E743", CheckCode::E902 => "E902", CheckCode::E999 => "E999", + // pycodestyle warnings + CheckCode::W292 => "W292", // pyflakes CheckCode::F401 => "F401", CheckCode::F402 => "F402", @@ -352,7 +362,7 @@ impl CheckCode { /// A placeholder representation of the CheckKind for the check. pub fn kind(&self) -> CheckKind { match self { - // pycodestyle + // pycodestyle errors CheckCode::E402 => CheckKind::ModuleImportNotAtTopOfFile, CheckCode::E501 => CheckKind::LineTooLong(89, 88), CheckCode::E711 => CheckKind::NoneComparison(RejectedCmpop::Eq), @@ -367,6 +377,8 @@ impl CheckCode { CheckCode::E743 => CheckKind::AmbiguousFunctionName("...".to_string()), CheckCode::E902 => CheckKind::IOError("IOError: `...`".to_string()), CheckCode::E999 => CheckKind::SyntaxError("`...`".to_string()), + // pycodestyle warnings + CheckCode::W292 => CheckKind::NoNewLineAtEndOfFile, // pyflakes CheckCode::F401 => CheckKind::UnusedImport(vec!["...".to_string()]), CheckCode::F402 => CheckKind::ImportShadowedByLoopVar("...".to_string(), 1), @@ -481,6 +493,8 @@ pub enum CheckKind { UnusedImport(Vec), UnusedVariable(String), YieldOutsideFunction, + // More style + NoNewLineAtEndOfFile, // flake8-builtin BuiltinVariableShadowing(String), BuiltinArgumentShadowing(String), @@ -551,6 +565,8 @@ impl CheckKind { CheckKind::UnusedImport(_) => "UnusedImport", CheckKind::UnusedVariable(_) => "UnusedVariable", CheckKind::YieldOutsideFunction => "YieldOutsideFunction", + // More style + CheckKind::NoNewLineAtEndOfFile => "NoNewLineAtEndOfFile", // flake8-builtins CheckKind::BuiltinVariableShadowing(_) => "BuiltinVariableShadowing", CheckKind::BuiltinArgumentShadowing(_) => "BuiltinArgumentShadowing", @@ -621,6 +637,8 @@ impl CheckKind { CheckKind::UnusedImport(_) => &CheckCode::F401, CheckKind::UnusedVariable(_) => &CheckCode::F841, CheckKind::YieldOutsideFunction => &CheckCode::F704, + // More style + CheckKind::NoNewLineAtEndOfFile => &CheckCode::W292, // flake8-builtins CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001, CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002, @@ -776,7 +794,8 @@ impl CheckKind { CheckKind::YieldOutsideFunction => { "`yield` or `yield from` statement outside of a function/method".to_string() } - + // More style + CheckKind::NoNewLineAtEndOfFile => "No newline at end of file".to_string(), // flake8-builtins CheckKind::BuiltinVariableShadowing(name) => { format!("Variable `{name}` is shadowing a python builtin") diff --git a/src/linter.rs b/src/linter.rs index 344089c6d7..5b43b60813 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -339,6 +339,30 @@ mod tests { Ok(()) } + #[test] + fn w292_0() -> Result<()> { + let mut checks = check_path( + Path::new("./resources/test/fixtures/W292_0.py"), + &settings::Settings::for_rule(CheckCode::W292), + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(checks); + Ok(()) + } + + #[test] + fn w292_1() -> Result<()> { + let mut checks = check_path( + Path::new("./resources/test/fixtures/W292_1.py"), + &settings::Settings::for_rule(CheckCode::W292), + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(checks); + Ok(()) + } + #[test] fn f401() -> Result<()> { let mut checks = check_path( diff --git a/src/snapshots/ruff__linter__tests__w292.snap b/src/snapshots/ruff__linter__tests__w292.snap new file mode 100644 index 0000000000..6f4412588e --- /dev/null +++ b/src/snapshots/ruff__linter__tests__w292.snap @@ -0,0 +1,13 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: NoNewLineAtEndOfFile + location: + row: 2 + column: 9 + end_location: + row: 2 + column: 9 + fix: ~ + diff --git a/src/snapshots/ruff__linter__tests__w292_0.snap b/src/snapshots/ruff__linter__tests__w292_0.snap new file mode 100644 index 0000000000..6f4412588e --- /dev/null +++ b/src/snapshots/ruff__linter__tests__w292_0.snap @@ -0,0 +1,13 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: NoNewLineAtEndOfFile + location: + row: 2 + column: 9 + end_location: + row: 2 + column: 9 + fix: ~ + diff --git a/src/snapshots/ruff__linter__tests__w292_1.snap b/src/snapshots/ruff__linter__tests__w292_1.snap new file mode 100644 index 0000000000..60c615f917 --- /dev/null +++ b/src/snapshots/ruff__linter__tests__w292_1.snap @@ -0,0 +1,6 @@ +--- +source: src/linter.rs +expression: checks +--- +[] +