diff --git a/crates/ruff/resources/test/fixtures/flake8_executable/EXE004_4.py b/crates/ruff/resources/test/fixtures/flake8_executable/EXE004_4.py new file mode 100755 index 0000000000..ae651325f7 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_executable/EXE004_4.py @@ -0,0 +1,2 @@ + + #!/usr/bin/env python diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index d51de2cae7..bef019d8b5 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -1,18 +1,12 @@ //! Lint rules based on checking physical lines. -use std::path::Path; - use ruff_text_size::TextSize; use ruff_diagnostics::Diagnostic; use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; use ruff_python_trivia::UniversalNewlines; -use crate::comments::shebang::ShebangDirective; use crate::registry::Rule; use crate::rules::flake8_copyright::rules::missing_copyright_notice; -use crate::rules::flake8_executable::rules::{ - shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace, -}; use crate::rules::pycodestyle::rules::{ doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file, tab_indentation, trailing_whitespace, @@ -22,7 +16,6 @@ use crate::rules::pyupgrade::rules::unnecessary_coding_comment; use crate::settings::Settings; pub(crate) fn check_physical_lines( - path: &Path, locator: &Locator, stylist: &Stylist, indexer: &Indexer, @@ -30,13 +23,7 @@ pub(crate) fn check_physical_lines( settings: &Settings, ) -> Vec { let mut diagnostics: Vec = vec![]; - let mut has_any_shebang = false; - let enforce_shebang_not_executable = settings.rules.enabled(Rule::ShebangNotExecutable); - let enforce_shebang_missing = settings.rules.enabled(Rule::ShebangMissingExecutableFile); - let enforce_shebang_whitespace = settings.rules.enabled(Rule::ShebangLeadingWhitespace); - let enforce_shebang_newline = settings.rules.enabled(Rule::ShebangNotFirstLine); - let enforce_shebang_python = settings.rules.enabled(Rule::ShebangMissingPython); let enforce_doc_line_too_long = settings.rules.enabled(Rule::DocLineTooLong); let enforce_line_too_long = settings.rules.enabled(Rule::LineTooLong); let enforce_no_newline_at_end_of_file = settings.rules.enabled(Rule::MissingNewlineAtEndOfFile); @@ -50,7 +37,6 @@ pub(crate) fn check_physical_lines( let enforce_copyright_notice = settings.rules.enabled(Rule::MissingCopyrightNotice); let fix_unnecessary_coding_comment = settings.rules.should_fix(Rule::UTF8EncodingDeclaration); - let fix_shebang_whitespace = settings.rules.should_fix(Rule::ShebangLeadingWhitespace); let mut commented_lines_iter = indexer.comment_ranges().iter().peekable(); let mut doc_lines_iter = doc_lines.iter().peekable(); @@ -69,43 +55,6 @@ pub(crate) fn check_physical_lines( } } } - - if enforce_shebang_missing - || enforce_shebang_not_executable - || enforce_shebang_whitespace - || enforce_shebang_newline - || enforce_shebang_python - { - if let Some(shebang) = ShebangDirective::try_extract(&line) { - has_any_shebang = true; - if enforce_shebang_not_executable { - if let Some(diagnostic) = - shebang_not_executable(path, line.range(), &shebang) - { - diagnostics.push(diagnostic); - } - } - if enforce_shebang_whitespace { - if let Some(diagnostic) = - shebang_whitespace(line.range(), &shebang, fix_shebang_whitespace) - { - diagnostics.push(diagnostic); - } - } - if enforce_shebang_newline { - if let Some(diagnostic) = - shebang_newline(line.range(), &shebang, index == 0) - { - diagnostics.push(diagnostic); - } - } - if enforce_shebang_python { - if let Some(diagnostic) = shebang_python(line.range(), &shebang) { - diagnostics.push(diagnostic); - } - } - } - } } while doc_lines_iter @@ -158,12 +107,6 @@ pub(crate) fn check_physical_lines( } } - if enforce_shebang_missing && !has_any_shebang { - if let Some(diagnostic) = shebang_missing(path) { - diagnostics.push(diagnostic); - } - } - if enforce_copyright_notice { if let Some(diagnostic) = missing_copyright_notice(locator, settings) { diagnostics.push(diagnostic); @@ -175,8 +118,6 @@ pub(crate) fn check_physical_lines( #[cfg(test)] mod tests { - use std::path::Path; - use rustpython_parser::lexer::lex; use rustpython_parser::Mode; @@ -198,7 +139,6 @@ mod tests { let check_with_max_line_length = |line_length: LineLength| { check_physical_lines( - Path::new("foo.py"), &locator, &stylist, &indexer, diff --git a/crates/ruff/src/checkers/tokens.rs b/crates/ruff/src/checkers/tokens.rs index 3479372085..06cfafd4a7 100644 --- a/crates/ruff/src/checkers/tokens.rs +++ b/crates/ruff/src/checkers/tokens.rs @@ -1,5 +1,7 @@ //! Lint rules based on token traversal. +use std::path::Path; + use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; @@ -11,15 +13,16 @@ use crate::lex::docstring_detection::StateMachine; use crate::registry::{AsRule, Rule}; use crate::rules::ruff::rules::Context; use crate::rules::{ - eradicate, flake8_commas, flake8_fixme, flake8_implicit_str_concat, flake8_pyi, flake8_quotes, - flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff, + eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat, + flake8_pyi, flake8_quotes, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff, }; use crate::settings::Settings; pub(crate) fn check_tokens( + tokens: &[LexResult], + path: &Path, locator: &Locator, indexer: &Indexer, - tokens: &[LexResult], settings: &Settings, is_stub: bool, ) -> Vec { @@ -143,6 +146,16 @@ pub(crate) fn check_tokens( flake8_pyi::rules::type_comment_in_stub(&mut diagnostics, locator, indexer); } + if settings.rules.any_enabled(&[ + Rule::ShebangNotExecutable, + Rule::ShebangMissingExecutableFile, + Rule::ShebangLeadingWhitespace, + Rule::ShebangNotFirstLine, + Rule::ShebangMissingPython, + ]) { + flake8_executable::rules::from_tokens(tokens, path, locator, settings, &mut diagnostics); + } + if settings.rules.any_enabled(&[ Rule::InvalidTodoTag, Rule::MissingTodoAuthor, diff --git a/crates/ruff/src/comments/shebang.rs b/crates/ruff/src/comments/shebang.rs index 1cac6f0adb..7a15e910f2 100644 --- a/crates/ruff/src/comments/shebang.rs +++ b/crates/ruff/src/comments/shebang.rs @@ -1,15 +1,10 @@ -use ruff_python_trivia::{is_python_whitespace, Cursor}; -use ruff_text_size::{TextLen, TextSize}; +use std::ops::Deref; + +use ruff_python_trivia::Cursor; /// A shebang directive (e.g., `#!/usr/bin/env python3`). #[derive(Debug, PartialEq, Eq)] -pub(crate) struct ShebangDirective<'a> { - /// The offset of the directive contents (e.g., `/usr/bin/env python3`) from the start of the - /// line. - pub(crate) offset: TextSize, - /// The contents of the directive (e.g., `"/usr/bin/env python3"`). - pub(crate) contents: &'a str, -} +pub(crate) struct ShebangDirective<'a>(&'a str); impl<'a> ShebangDirective<'a> { /// Parse a shebang directive from a line, or return `None` if the line does not contain a @@ -17,9 +12,6 @@ impl<'a> ShebangDirective<'a> { pub(crate) fn try_extract(line: &'a str) -> Option { let mut cursor = Cursor::new(line); - // Trim whitespace. - cursor.eat_while(is_python_whitespace); - // Trim the `#!` prefix. if !cursor.eat_char('#') { return None; @@ -28,10 +20,15 @@ impl<'a> ShebangDirective<'a> { return None; } - Some(Self { - offset: line.text_len() - cursor.text_len(), - contents: cursor.chars().as_str(), - }) + Some(Self(cursor.chars().as_str())) + } +} + +impl Deref for ShebangDirective<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0 } } @@ -59,6 +56,12 @@ mod tests { assert_debug_snapshot!(ShebangDirective::try_extract(source)); } + #[test] + fn shebang_match_trailing_comment() { + let source = "#!/usr/bin/env python # trailing comment"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } + #[test] fn shebang_leading_space() { let source = " #!/usr/bin/env python"; diff --git a/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap index 8ea8bfcfca..87d72c9d88 100644 --- a/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap @@ -2,9 +2,4 @@ source: crates/ruff/src/comments/shebang.rs expression: "ShebangDirective::try_extract(source)" --- -Some( - ShebangDirective { - offset: 4, - contents: "/usr/bin/env python", - }, -) +None diff --git a/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap index c0ec6ca308..9078c810d1 100644 --- a/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap @@ -3,8 +3,7 @@ source: crates/ruff/src/comments/shebang.rs expression: "ShebangDirective::try_extract(source)" --- Some( - ShebangDirective { - offset: 2, - contents: "/usr/bin/env python", - }, + ShebangDirective( + "/usr/bin/env python", + ), ) diff --git a/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match_trailing_comment.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match_trailing_comment.snap new file mode 100644 index 0000000000..c309559855 --- /dev/null +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match_trailing_comment.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff/src/comments/shebang.rs +expression: "ShebangDirective::try_extract(source)" +--- +Some( + ShebangDirective( + "/usr/bin/env python # trailing comment", + ), +) diff --git a/crates/ruff/src/linter.rs b/crates/ruff/src/linter.rs index 9a42d37a3f..768fb36faa 100644 --- a/crates/ruff/src/linter.rs +++ b/crates/ruff/src/linter.rs @@ -100,7 +100,9 @@ pub fn check_path( .any(|rule_code| rule_code.lint_source().is_tokens()) { let is_stub = is_python_stub_file(path); - diagnostics.extend(check_tokens(locator, indexer, &tokens, settings, is_stub)); + diagnostics.extend(check_tokens( + &tokens, path, locator, indexer, settings, is_stub, + )); } // Run the filesystem-based rules. @@ -193,7 +195,7 @@ pub fn check_path( .any(|rule_code| rule_code.lint_source().is_physical_lines()) { diagnostics.extend(check_physical_lines( - path, locator, stylist, indexer, &doc_lines, settings, + locator, stylist, indexer, &doc_lines, settings, )); } diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index db324714f3..f4d5bd2b60 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -238,22 +238,16 @@ impl Rule { match self { Rule::InvalidPyprojectToml => LintSource::PyprojectToml, Rule::UnusedNOQA => LintSource::Noqa, - - Rule::DocLineTooLong + Rule::BidirectionalUnicode + | Rule::BlankLineWithWhitespace + | Rule::DocLineTooLong | Rule::LineTooLong - | Rule::MixedSpacesAndTabs - | Rule::MissingNewlineAtEndOfFile - | Rule::UTF8EncodingDeclaration - | Rule::ShebangMissingExecutableFile - | Rule::ShebangNotExecutable - | Rule::ShebangNotFirstLine - | Rule::BidirectionalUnicode - | Rule::ShebangMissingPython - | Rule::ShebangLeadingWhitespace - | Rule::TrailingWhitespace - | Rule::TabIndentation | Rule::MissingCopyrightNotice - | Rule::BlankLineWithWhitespace => LintSource::PhysicalLines, + | Rule::MissingNewlineAtEndOfFile + | Rule::MixedSpacesAndTabs + | Rule::TabIndentation + | Rule::TrailingWhitespace + | Rule::UTF8EncodingDeclaration => LintSource::PhysicalLines, Rule::AmbiguousUnicodeCharacterComment | Rule::AmbiguousUnicodeCharacterDocstring | Rule::AmbiguousUnicodeCharacterString @@ -264,33 +258,38 @@ impl Rule { | Rule::BlanketNOQA | Rule::BlanketTypeIgnore | Rule::CommentedOutCode - | Rule::MultiLineImplicitStringConcatenation + | Rule::ExtraneousParentheses | Rule::InvalidCharacterBackspace - | Rule::InvalidCharacterSub | Rule::InvalidCharacterEsc | Rule::InvalidCharacterNul + | Rule::InvalidCharacterSub | Rule::InvalidCharacterZeroWidthSpace - | Rule::ExtraneousParentheses | Rule::InvalidEscapeSequence - | Rule::SingleLineImplicitStringConcatenation - | Rule::MissingTrailingComma - | Rule::TrailingCommaOnBareTuple - | Rule::MultipleStatementsOnOneLineColon - | Rule::UselessSemicolon - | Rule::MultipleStatementsOnOneLineSemicolon - | Rule::ProhibitedTrailingComma - | Rule::TypeCommentInStub - | Rule::InvalidTodoTag - | Rule::MissingTodoAuthor - | Rule::MissingTodoLink - | Rule::MissingTodoColon - | Rule::MissingTodoDescription | Rule::InvalidTodoCapitalization - | Rule::MissingSpaceAfterTodoColon + | Rule::InvalidTodoTag | Rule::LineContainsFixme | Rule::LineContainsHack | Rule::LineContainsTodo - | Rule::LineContainsXxx => LintSource::Tokens, + | Rule::LineContainsXxx + | Rule::MissingSpaceAfterTodoColon + | Rule::MissingTodoAuthor + | Rule::MissingTodoColon + | Rule::MissingTodoDescription + | Rule::MissingTodoLink + | Rule::MissingTrailingComma + | Rule::MultiLineImplicitStringConcatenation + | Rule::MultipleStatementsOnOneLineColon + | Rule::MultipleStatementsOnOneLineSemicolon + | Rule::ProhibitedTrailingComma + | Rule::ShebangLeadingWhitespace + | Rule::ShebangMissingExecutableFile + | Rule::ShebangMissingPython + | Rule::ShebangNotExecutable + | Rule::ShebangNotFirstLine + | Rule::SingleLineImplicitStringConcatenation + | Rule::TrailingCommaOnBareTuple + | Rule::TypeCommentInStub + | Rule::UselessSemicolon => LintSource::Tokens, Rule::IOError => LintSource::Io, Rule::UnsortedImports | Rule::MissingRequiredImport => LintSource::Imports, Rule::ImplicitNamespacePackage | Rule::InvalidModuleName => LintSource::Filesystem, diff --git a/crates/ruff/src/rules/flake8_executable/mod.rs b/crates/ruff/src/rules/flake8_executable/mod.rs index a1bab8efe2..25eb88bf5e 100644 --- a/crates/ruff/src/rules/flake8_executable/mod.rs +++ b/crates/ruff/src/rules/flake8_executable/mod.rs @@ -24,6 +24,7 @@ mod tests { #[test_case(Path::new("EXE004_1.py"))] #[test_case(Path::new("EXE004_2.py"))] #[test_case(Path::new("EXE004_3.py"))] + #[test_case(Path::new("EXE004_4.py"))] #[test_case(Path::new("EXE005_1.py"))] #[test_case(Path::new("EXE005_2.py"))] #[test_case(Path::new("EXE005_3.py"))] diff --git a/crates/ruff/src/rules/flake8_executable/rules/mod.rs b/crates/ruff/src/rules/flake8_executable/rules/mod.rs index 35b1aa269e..f854387424 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/mod.rs @@ -1,11 +1,60 @@ -pub(crate) use shebang_missing::*; -pub(crate) use shebang_newline::*; -pub(crate) use shebang_not_executable::*; -pub(crate) use shebang_python::*; -pub(crate) use shebang_whitespace::*; +use std::path::Path; -mod shebang_missing; -mod shebang_newline; +use rustpython_parser::lexer::LexResult; +use rustpython_parser::Tok; + +use ruff_diagnostics::Diagnostic; +use ruff_python_ast::source_code::Locator; +pub(crate) use shebang_leading_whitespace::*; +pub(crate) use shebang_missing_executable_file::*; +pub(crate) use shebang_missing_python::*; +pub(crate) use shebang_not_executable::*; +pub(crate) use shebang_not_first_line::*; + +use crate::comments::shebang::ShebangDirective; +use crate::settings::Settings; + +mod shebang_leading_whitespace; +mod shebang_missing_executable_file; +mod shebang_missing_python; mod shebang_not_executable; -mod shebang_python; -mod shebang_whitespace; +mod shebang_not_first_line; + +pub(crate) fn from_tokens( + tokens: &[LexResult], + path: &Path, + locator: &Locator, + settings: &Settings, + diagnostics: &mut Vec, +) { + let mut has_any_shebang = false; + for (tok, range) in tokens.iter().flatten() { + if let Tok::Comment(comment) = tok { + if let Some(shebang) = ShebangDirective::try_extract(comment) { + has_any_shebang = true; + + if let Some(diagnostic) = shebang_missing_python(*range, &shebang) { + diagnostics.push(diagnostic); + } + + if let Some(diagnostic) = shebang_not_executable(path, *range) { + diagnostics.push(diagnostic); + } + + if let Some(diagnostic) = shebang_leading_whitespace(*range, locator, settings) { + diagnostics.push(diagnostic); + } + + if let Some(diagnostic) = shebang_not_first_line(*range, locator) { + diagnostics.push(diagnostic); + } + } + } + } + + if !has_any_shebang { + if let Some(diagnostic) = shebang_missing_executable_file(path) { + diagnostics.push(diagnostic); + } + } +} diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_leading_whitespace.rs similarity index 59% rename from crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs rename to crates/ruff/src/rules/flake8_executable/rules/shebang_leading_whitespace.rs index 3b8c990897..dae3757d67 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_leading_whitespace.rs @@ -1,11 +1,12 @@ -use std::ops::Sub; - use ruff_text_size::{TextRange, TextSize}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::source_code::Locator; +use ruff_python_trivia::is_python_whitespace; -use crate::comments::shebang::ShebangDirective; +use crate::registry::AsRule; +use crate::settings::Settings; /// ## What it does /// Checks for whitespace before a shebang directive. @@ -46,31 +47,29 @@ impl AlwaysAutofixableViolation for ShebangLeadingWhitespace { } /// EXE004 -pub(crate) fn shebang_whitespace( +pub(crate) fn shebang_leading_whitespace( range: TextRange, - shebang: &ShebangDirective, - autofix: bool, + locator: &Locator, + settings: &Settings, ) -> Option { - let ShebangDirective { - offset, - contents: _, - } = shebang; - - if *offset > TextSize::from(2) { - let leading_space_start = range.start(); - let leading_space_len = offset.sub(TextSize::new(2)); - let mut diagnostic = Diagnostic::new( - ShebangLeadingWhitespace, - TextRange::at(leading_space_start, leading_space_len), - ); - if autofix { - diagnostic.set_fix(Fix::automatic(Edit::range_deletion(TextRange::at( - leading_space_start, - leading_space_len, - )))); - } - Some(diagnostic) - } else { - None + // If the shebang is at the beginning of the file, abort. + if range.start() == TextSize::from(0) { + return None; } + + // If the entire prefix _isn't_ whitespace, abort (this is handled by EXE005). + if !locator + .up_to(range.start()) + .chars() + .all(|c| is_python_whitespace(c) || matches!(c, '\r' | '\n')) + { + return None; + } + + let prefix = TextRange::up_to(range.start()); + let mut diagnostic = Diagnostic::new(ShebangLeadingWhitespace, prefix); + if settings.rules.should_fix(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(prefix))); + } + Some(diagnostic) } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_missing_executable_file.rs similarity index 84% rename from crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs rename to crates/ruff/src/rules/flake8_executable/rules/shebang_missing_executable_file.rs index 2f1e47f015..6ad2e3e202 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_missing_executable_file.rs @@ -2,9 +2,8 @@ use std::path::Path; -use wsl; - use ruff_text_size::TextRange; +use wsl; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -43,20 +42,22 @@ impl Violation for ShebangMissingExecutableFile { /// EXE002 #[cfg(target_family = "unix")] -pub(crate) fn shebang_missing(filepath: &Path) -> Option { +pub(crate) fn shebang_missing_executable_file(filepath: &Path) -> Option { // WSL supports Windows file systems, which do not have executable bits. // Instead, everything is executable. Therefore, we skip this rule on WSL. if wsl::is_wsl() { return None; } if let Ok(true) = is_executable(filepath) { - let diagnostic = Diagnostic::new(ShebangMissingExecutableFile, TextRange::default()); - return Some(diagnostic); + return Some(Diagnostic::new( + ShebangMissingExecutableFile, + TextRange::default(), + )); } None } #[cfg(not(target_family = "unix"))] -pub(crate) fn shebang_missing(_filepath: &Path) -> Option { +pub(crate) fn shebang_missing_executable_file(_filepath: &Path) -> Option { None } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_missing_python.rs similarity index 72% rename from crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs rename to crates/ruff/src/rules/flake8_executable/rules/shebang_missing_python.rs index 832533d8ca..b19e69c098 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_missing_python.rs @@ -1,4 +1,4 @@ -use ruff_text_size::{TextLen, TextRange, TextSize}; +use ruff_text_size::TextRange; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -41,15 +41,13 @@ impl Violation for ShebangMissingPython { } /// EXE003 -pub(crate) fn shebang_python(range: TextRange, shebang: &ShebangDirective) -> Option { - let ShebangDirective { offset, contents } = shebang; - - if contents.contains("python") || contents.contains("pytest") { - None - } else { - Some(Diagnostic::new( - ShebangMissingPython, - TextRange::at(range.start() + offset, contents.text_len()).sub_start(TextSize::from(2)), - )) +pub(crate) fn shebang_missing_python( + range: TextRange, + shebang: &ShebangDirective, +) -> Option { + if shebang.contains("python") || shebang.contains("pytest") { + return None; } + + Some(Diagnostic::new(ShebangMissingPython, range)) } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs index a1400f0b67..4df36e3335 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs @@ -2,15 +2,12 @@ use std::path::Path; +use ruff_text_size::TextRange; use wsl; -use ruff_text_size::{TextLen, TextRange, TextSize}; - use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use crate::comments::shebang::ShebangDirective; -use crate::registry::AsRule; #[cfg(target_family = "unix")] use crate::rules::flake8_executable::helpers::is_executable; @@ -45,34 +42,21 @@ impl Violation for ShebangNotExecutable { /// EXE001 #[cfg(target_family = "unix")] -pub(crate) fn shebang_not_executable( - filepath: &Path, - range: TextRange, - shebang: &ShebangDirective, -) -> Option { +pub(crate) fn shebang_not_executable(filepath: &Path, range: TextRange) -> Option { // WSL supports Windows file systems, which do not have executable bits. // Instead, everything is executable. Therefore, we skip this rule on WSL. if wsl::is_wsl() { return None; } - let ShebangDirective { offset, contents } = shebang; if let Ok(false) = is_executable(filepath) { - let diagnostic = Diagnostic::new( - ShebangNotExecutable, - TextRange::at(range.start() + offset, contents.text_len()), - ); - return Some(diagnostic); + return Some(Diagnostic::new(ShebangNotExecutable, range)); } None } #[cfg(not(target_family = "unix"))] -pub(crate) fn shebang_not_executable( - _filepath: &Path, - _range: TextRange, - _shebang: &ShebangDirective, -) -> Option { +pub(crate) fn shebang_not_executable(_filepath: &Path, _range: TextRange) -> Option { None } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_first_line.rs similarity index 63% rename from crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs rename to crates/ruff/src/rules/flake8_executable/rules/shebang_not_first_line.rs index 626e29020a..2d8ee35863 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_first_line.rs @@ -1,9 +1,9 @@ -use ruff_text_size::{TextLen, TextRange}; +use ruff_text_size::{TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; - -use crate::comments::shebang::ShebangDirective; +use ruff_python_ast::source_code::Locator; +use ruff_python_trivia::is_python_whitespace; /// ## What it does /// Checks for a shebang directive that is not at the beginning of the file. @@ -42,19 +42,20 @@ impl Violation for ShebangNotFirstLine { } /// EXE005 -pub(crate) fn shebang_newline( - range: TextRange, - shebang: &ShebangDirective, - first_line: bool, -) -> Option { - let ShebangDirective { offset, contents } = shebang; - - if first_line { - None - } else { - Some(Diagnostic::new( - ShebangNotFirstLine, - TextRange::at(range.start() + offset, contents.text_len()), - )) +pub(crate) fn shebang_not_first_line(range: TextRange, locator: &Locator) -> Option { + // If the shebang is at the beginning of the file, abort. + if range.start() == TextSize::from(0) { + return None; } + + // If the entire prefix is whitespace, abort (this is handled by EXE004). + if locator + .up_to(range.start()) + .chars() + .all(|c| is_python_whitespace(c) || matches!(c, '\r' | '\n')) + { + return None; + } + + Some(Diagnostic::new(ShebangNotFirstLine, range)) } diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_1.py.snap b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_1.py.snap index 4d287f67bc..ce8d160f86 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_1.py.snap +++ b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_1.py.snap @@ -1,10 +1,10 @@ --- source: crates/ruff/src/rules/flake8_executable/mod.rs --- -EXE001_1.py:1:3: EXE001 Shebang is present but file is not executable +EXE001_1.py:1:1: EXE001 Shebang is present but file is not executable | 1 | #!/usr/bin/python - | ^^^^^^^^^^^^^^^ EXE001 + | ^^^^^^^^^^^^^^^^^ EXE001 2 | 3 | if __name__ == '__main__': | diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_3.py.snap b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_3.py.snap index 974ea12679..7a1b02bc25 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_3.py.snap +++ b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_3.py.snap @@ -1,11 +1,10 @@ --- source: crates/ruff/src/rules/flake8_executable/mod.rs --- -EXE004_3.py:1:1: EXE002 The file is executable but no shebang is present +EXE004_3.py:2:7: EXE005 Shebang should be at the beginning of the file | -1 | - | EXE002 2 | pass #!/usr/bin/env python + | ^^^^^^^^^^^^^^^^^^^^^ EXE005 | diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_4.py.snap b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_4.py.snap new file mode 100644 index 0000000000..06f93a1885 --- /dev/null +++ b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_4.py.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff/src/rules/flake8_executable/mod.rs +--- +EXE004_4.py:1:1: EXE004 [*] Avoid whitespace before shebang + | +1 | / +2 | | #!/usr/bin/env python + | |____^ EXE004 + | + = help: Remove whitespace before shebang + +ℹ Fix +1 |- +2 |- #!/usr/bin/env python + 1 |+#!/usr/bin/env python + + diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE005_1.py.snap b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE005_1.py.snap index 96ac1dd06d..510c0ee5f9 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE005_1.py.snap +++ b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE005_1.py.snap @@ -1,11 +1,11 @@ --- source: crates/ruff/src/rules/flake8_executable/mod.rs --- -EXE005_1.py:3:3: EXE005 Shebang should be at the beginning of the file +EXE005_1.py:3:1: EXE005 Shebang should be at the beginning of the file | 2 | # A python comment 3 | #!/usr/bin/python - | ^^^^^^^^^^^^^^^ EXE005 + | ^^^^^^^^^^^^^^^^^ EXE005 | diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE005_2.py.snap b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE005_2.py.snap index 9345f7e335..124bcea10e 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE005_2.py.snap +++ b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE005_2.py.snap @@ -1,11 +1,11 @@ --- source: crates/ruff/src/rules/flake8_executable/mod.rs --- -EXE005_2.py:4:3: EXE005 Shebang should be at the beginning of the file +EXE005_2.py:4:1: EXE005 Shebang should be at the beginning of the file | 3 | # A python comment 4 | #!/usr/bin/python - | ^^^^^^^^^^^^^^^ EXE005 + | ^^^^^^^^^^^^^^^^^ EXE005 | diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE005_3.py.snap b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE005_3.py.snap index 6253d085d1..44fadb275d 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE005_3.py.snap +++ b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE005_3.py.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_executable/mod.rs --- -EXE005_3.py:6:3: EXE005 Shebang should be at the beginning of the file +EXE005_3.py:6:1: EXE005 Shebang should be at the beginning of the file | 4 | """ 5 | # A python comment 6 | #!/usr/bin/python - | ^^^^^^^^^^^^^^^ EXE005 + | ^^^^^^^^^^^^^^^^^ EXE005 |