Move flake8-executable rules out of physical lines checker (#6039)

## Summary

These only need the token stream, and we always prefer token-based to
physical line-based rules.

There are a few other changes snuck in here:

- Renaming the rule files to match the diagnostic names (likely an
error).
- The "leading whitespace before shebang" rule now works regardless of
where the comment occurs (i.e., if the shebang is on the second line,
and the first line is blank, we flag and remove that leading
whitespace).
This commit is contained in:
Charlie Marsh 2023-07-24 14:38:05 -04:00 committed by GitHub
parent 7f3797185c
commit 776d598738
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 235 additions and 224 deletions

View file

@ -0,0 +1,2 @@
#!/usr/bin/env python

View file

@ -1,18 +1,12 @@
//! Lint rules based on checking physical lines. //! Lint rules based on checking physical lines.
use std::path::Path;
use ruff_text_size::TextSize; use ruff_text_size::TextSize;
use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Diagnostic;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_trivia::UniversalNewlines; use ruff_python_trivia::UniversalNewlines;
use crate::comments::shebang::ShebangDirective;
use crate::registry::Rule; use crate::registry::Rule;
use crate::rules::flake8_copyright::rules::missing_copyright_notice; 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::{ use crate::rules::pycodestyle::rules::{
doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file, doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file,
tab_indentation, trailing_whitespace, tab_indentation, trailing_whitespace,
@ -22,7 +16,6 @@ use crate::rules::pyupgrade::rules::unnecessary_coding_comment;
use crate::settings::Settings; use crate::settings::Settings;
pub(crate) fn check_physical_lines( pub(crate) fn check_physical_lines(
path: &Path,
locator: &Locator, locator: &Locator,
stylist: &Stylist, stylist: &Stylist,
indexer: &Indexer, indexer: &Indexer,
@ -30,13 +23,7 @@ pub(crate) fn check_physical_lines(
settings: &Settings, settings: &Settings,
) -> Vec<Diagnostic> { ) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![]; let mut diagnostics: Vec<Diagnostic> = 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_doc_line_too_long = settings.rules.enabled(Rule::DocLineTooLong);
let enforce_line_too_long = settings.rules.enabled(Rule::LineTooLong); let enforce_line_too_long = settings.rules.enabled(Rule::LineTooLong);
let enforce_no_newline_at_end_of_file = settings.rules.enabled(Rule::MissingNewlineAtEndOfFile); 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 enforce_copyright_notice = settings.rules.enabled(Rule::MissingCopyrightNotice);
let fix_unnecessary_coding_comment = settings.rules.should_fix(Rule::UTF8EncodingDeclaration); 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 commented_lines_iter = indexer.comment_ranges().iter().peekable();
let mut doc_lines_iter = doc_lines.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 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 enforce_copyright_notice {
if let Some(diagnostic) = missing_copyright_notice(locator, settings) { if let Some(diagnostic) = missing_copyright_notice(locator, settings) {
diagnostics.push(diagnostic); diagnostics.push(diagnostic);
@ -175,8 +118,6 @@ pub(crate) fn check_physical_lines(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::path::Path;
use rustpython_parser::lexer::lex; use rustpython_parser::lexer::lex;
use rustpython_parser::Mode; use rustpython_parser::Mode;
@ -198,7 +139,6 @@ mod tests {
let check_with_max_line_length = |line_length: LineLength| { let check_with_max_line_length = |line_length: LineLength| {
check_physical_lines( check_physical_lines(
Path::new("foo.py"),
&locator, &locator,
&stylist, &stylist,
&indexer, &indexer,

View file

@ -1,5 +1,7 @@
//! Lint rules based on token traversal. //! Lint rules based on token traversal.
use std::path::Path;
use rustpython_parser::lexer::LexResult; use rustpython_parser::lexer::LexResult;
use rustpython_parser::Tok; use rustpython_parser::Tok;
@ -11,15 +13,16 @@ use crate::lex::docstring_detection::StateMachine;
use crate::registry::{AsRule, Rule}; use crate::registry::{AsRule, Rule};
use crate::rules::ruff::rules::Context; use crate::rules::ruff::rules::Context;
use crate::rules::{ use crate::rules::{
eradicate, flake8_commas, flake8_fixme, flake8_implicit_str_concat, flake8_pyi, flake8_quotes, eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff, flake8_pyi, flake8_quotes, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff,
}; };
use crate::settings::Settings; use crate::settings::Settings;
pub(crate) fn check_tokens( pub(crate) fn check_tokens(
tokens: &[LexResult],
path: &Path,
locator: &Locator, locator: &Locator,
indexer: &Indexer, indexer: &Indexer,
tokens: &[LexResult],
settings: &Settings, settings: &Settings,
is_stub: bool, is_stub: bool,
) -> Vec<Diagnostic> { ) -> Vec<Diagnostic> {
@ -143,6 +146,16 @@ pub(crate) fn check_tokens(
flake8_pyi::rules::type_comment_in_stub(&mut diagnostics, locator, indexer); 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(&[ if settings.rules.any_enabled(&[
Rule::InvalidTodoTag, Rule::InvalidTodoTag,
Rule::MissingTodoAuthor, Rule::MissingTodoAuthor,

View file

@ -1,15 +1,10 @@
use ruff_python_trivia::{is_python_whitespace, Cursor}; use std::ops::Deref;
use ruff_text_size::{TextLen, TextSize};
use ruff_python_trivia::Cursor;
/// A shebang directive (e.g., `#!/usr/bin/env python3`). /// A shebang directive (e.g., `#!/usr/bin/env python3`).
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub(crate) struct ShebangDirective<'a> { pub(crate) struct ShebangDirective<'a>(&'a str);
/// 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,
}
impl<'a> ShebangDirective<'a> { impl<'a> ShebangDirective<'a> {
/// Parse a shebang directive from a line, or return `None` if the line does not contain 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<Self> { pub(crate) fn try_extract(line: &'a str) -> Option<Self> {
let mut cursor = Cursor::new(line); let mut cursor = Cursor::new(line);
// Trim whitespace.
cursor.eat_while(is_python_whitespace);
// Trim the `#!` prefix. // Trim the `#!` prefix.
if !cursor.eat_char('#') { if !cursor.eat_char('#') {
return None; return None;
@ -28,10 +20,15 @@ impl<'a> ShebangDirective<'a> {
return None; return None;
} }
Some(Self { Some(Self(cursor.chars().as_str()))
offset: line.text_len() - cursor.text_len(), }
contents: 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)); 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] #[test]
fn shebang_leading_space() { fn shebang_leading_space() {
let source = " #!/usr/bin/env python"; let source = " #!/usr/bin/env python";

View file

@ -2,9 +2,4 @@
source: crates/ruff/src/comments/shebang.rs source: crates/ruff/src/comments/shebang.rs
expression: "ShebangDirective::try_extract(source)" expression: "ShebangDirective::try_extract(source)"
--- ---
Some( None
ShebangDirective {
offset: 4,
contents: "/usr/bin/env python",
},
)

View file

@ -3,8 +3,7 @@ source: crates/ruff/src/comments/shebang.rs
expression: "ShebangDirective::try_extract(source)" expression: "ShebangDirective::try_extract(source)"
--- ---
Some( Some(
ShebangDirective { ShebangDirective(
offset: 2, "/usr/bin/env python",
contents: "/usr/bin/env python", ),
},
) )

View file

@ -0,0 +1,9 @@
---
source: crates/ruff/src/comments/shebang.rs
expression: "ShebangDirective::try_extract(source)"
---
Some(
ShebangDirective(
"/usr/bin/env python # trailing comment",
),
)

View file

@ -100,7 +100,9 @@ pub fn check_path(
.any(|rule_code| rule_code.lint_source().is_tokens()) .any(|rule_code| rule_code.lint_source().is_tokens())
{ {
let is_stub = is_python_stub_file(path); 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. // Run the filesystem-based rules.
@ -193,7 +195,7 @@ pub fn check_path(
.any(|rule_code| rule_code.lint_source().is_physical_lines()) .any(|rule_code| rule_code.lint_source().is_physical_lines())
{ {
diagnostics.extend(check_physical_lines( diagnostics.extend(check_physical_lines(
path, locator, stylist, indexer, &doc_lines, settings, locator, stylist, indexer, &doc_lines, settings,
)); ));
} }

View file

@ -238,22 +238,16 @@ impl Rule {
match self { match self {
Rule::InvalidPyprojectToml => LintSource::PyprojectToml, Rule::InvalidPyprojectToml => LintSource::PyprojectToml,
Rule::UnusedNOQA => LintSource::Noqa, Rule::UnusedNOQA => LintSource::Noqa,
Rule::BidirectionalUnicode
Rule::DocLineTooLong | Rule::BlankLineWithWhitespace
| Rule::DocLineTooLong
| Rule::LineTooLong | 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::MissingCopyrightNotice
| Rule::BlankLineWithWhitespace => LintSource::PhysicalLines, | Rule::MissingNewlineAtEndOfFile
| Rule::MixedSpacesAndTabs
| Rule::TabIndentation
| Rule::TrailingWhitespace
| Rule::UTF8EncodingDeclaration => LintSource::PhysicalLines,
Rule::AmbiguousUnicodeCharacterComment Rule::AmbiguousUnicodeCharacterComment
| Rule::AmbiguousUnicodeCharacterDocstring | Rule::AmbiguousUnicodeCharacterDocstring
| Rule::AmbiguousUnicodeCharacterString | Rule::AmbiguousUnicodeCharacterString
@ -264,33 +258,38 @@ impl Rule {
| Rule::BlanketNOQA | Rule::BlanketNOQA
| Rule::BlanketTypeIgnore | Rule::BlanketTypeIgnore
| Rule::CommentedOutCode | Rule::CommentedOutCode
| Rule::MultiLineImplicitStringConcatenation | Rule::ExtraneousParentheses
| Rule::InvalidCharacterBackspace | Rule::InvalidCharacterBackspace
| Rule::InvalidCharacterSub
| Rule::InvalidCharacterEsc | Rule::InvalidCharacterEsc
| Rule::InvalidCharacterNul | Rule::InvalidCharacterNul
| Rule::InvalidCharacterSub
| Rule::InvalidCharacterZeroWidthSpace | Rule::InvalidCharacterZeroWidthSpace
| Rule::ExtraneousParentheses
| Rule::InvalidEscapeSequence | 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::InvalidTodoCapitalization
| Rule::MissingSpaceAfterTodoColon | Rule::InvalidTodoTag
| Rule::LineContainsFixme | Rule::LineContainsFixme
| Rule::LineContainsHack | Rule::LineContainsHack
| Rule::LineContainsTodo | 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::IOError => LintSource::Io,
Rule::UnsortedImports | Rule::MissingRequiredImport => LintSource::Imports, Rule::UnsortedImports | Rule::MissingRequiredImport => LintSource::Imports,
Rule::ImplicitNamespacePackage | Rule::InvalidModuleName => LintSource::Filesystem, Rule::ImplicitNamespacePackage | Rule::InvalidModuleName => LintSource::Filesystem,

View file

@ -24,6 +24,7 @@ mod tests {
#[test_case(Path::new("EXE004_1.py"))] #[test_case(Path::new("EXE004_1.py"))]
#[test_case(Path::new("EXE004_2.py"))] #[test_case(Path::new("EXE004_2.py"))]
#[test_case(Path::new("EXE004_3.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_1.py"))]
#[test_case(Path::new("EXE005_2.py"))] #[test_case(Path::new("EXE005_2.py"))]
#[test_case(Path::new("EXE005_3.py"))] #[test_case(Path::new("EXE005_3.py"))]

View file

@ -1,11 +1,60 @@
pub(crate) use shebang_missing::*; use std::path::Path;
pub(crate) use shebang_newline::*;
pub(crate) use shebang_not_executable::*;
pub(crate) use shebang_python::*;
pub(crate) use shebang_whitespace::*;
mod shebang_missing; use rustpython_parser::lexer::LexResult;
mod shebang_newline; 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_not_executable;
mod shebang_python; mod shebang_not_first_line;
mod shebang_whitespace;
pub(crate) fn from_tokens(
tokens: &[LexResult],
path: &Path,
locator: &Locator,
settings: &Settings,
diagnostics: &mut Vec<Diagnostic>,
) {
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);
}
}
}

View file

@ -1,11 +1,12 @@
use std::ops::Sub;
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation}; 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 /// ## What it does
/// Checks for whitespace before a shebang directive. /// Checks for whitespace before a shebang directive.
@ -46,31 +47,29 @@ impl AlwaysAutofixableViolation for ShebangLeadingWhitespace {
} }
/// EXE004 /// EXE004
pub(crate) fn shebang_whitespace( pub(crate) fn shebang_leading_whitespace(
range: TextRange, range: TextRange,
shebang: &ShebangDirective, locator: &Locator,
autofix: bool, settings: &Settings,
) -> Option<Diagnostic> { ) -> Option<Diagnostic> {
let ShebangDirective { // If the shebang is at the beginning of the file, abort.
offset, if range.start() == TextSize::from(0) {
contents: _, return None;
} = 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 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)
} }

View file

@ -2,9 +2,8 @@
use std::path::Path; use std::path::Path;
use wsl;
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
use wsl;
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
@ -43,20 +42,22 @@ impl Violation for ShebangMissingExecutableFile {
/// EXE002 /// EXE002
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
pub(crate) fn shebang_missing(filepath: &Path) -> Option<Diagnostic> { pub(crate) fn shebang_missing_executable_file(filepath: &Path) -> Option<Diagnostic> {
// WSL supports Windows file systems, which do not have executable bits. // WSL supports Windows file systems, which do not have executable bits.
// Instead, everything is executable. Therefore, we skip this rule on WSL. // Instead, everything is executable. Therefore, we skip this rule on WSL.
if wsl::is_wsl() { if wsl::is_wsl() {
return None; return None;
} }
if let Ok(true) = is_executable(filepath) { if let Ok(true) = is_executable(filepath) {
let diagnostic = Diagnostic::new(ShebangMissingExecutableFile, TextRange::default()); return Some(Diagnostic::new(
return Some(diagnostic); ShebangMissingExecutableFile,
TextRange::default(),
));
} }
None None
} }
#[cfg(not(target_family = "unix"))] #[cfg(not(target_family = "unix"))]
pub(crate) fn shebang_missing(_filepath: &Path) -> Option<Diagnostic> { pub(crate) fn shebang_missing_executable_file(_filepath: &Path) -> Option<Diagnostic> {
None None
} }

View file

@ -1,4 +1,4 @@
use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
@ -41,15 +41,13 @@ impl Violation for ShebangMissingPython {
} }
/// EXE003 /// EXE003
pub(crate) fn shebang_python(range: TextRange, shebang: &ShebangDirective) -> Option<Diagnostic> { pub(crate) fn shebang_missing_python(
let ShebangDirective { offset, contents } = shebang; range: TextRange,
shebang: &ShebangDirective,
if contents.contains("python") || contents.contains("pytest") { ) -> Option<Diagnostic> {
None if shebang.contains("python") || shebang.contains("pytest") {
} else { return None;
Some(Diagnostic::new(
ShebangMissingPython,
TextRange::at(range.start() + offset, contents.text_len()).sub_start(TextSize::from(2)),
))
} }
Some(Diagnostic::new(ShebangMissingPython, range))
} }

View file

@ -2,15 +2,12 @@
use std::path::Path; use std::path::Path;
use ruff_text_size::TextRange;
use wsl; use wsl;
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use crate::comments::shebang::ShebangDirective;
use crate::registry::AsRule;
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
use crate::rules::flake8_executable::helpers::is_executable; use crate::rules::flake8_executable::helpers::is_executable;
@ -45,34 +42,21 @@ impl Violation for ShebangNotExecutable {
/// EXE001 /// EXE001
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
pub(crate) fn shebang_not_executable( pub(crate) fn shebang_not_executable(filepath: &Path, range: TextRange) -> Option<Diagnostic> {
filepath: &Path,
range: TextRange,
shebang: &ShebangDirective,
) -> Option<Diagnostic> {
// WSL supports Windows file systems, which do not have executable bits. // WSL supports Windows file systems, which do not have executable bits.
// Instead, everything is executable. Therefore, we skip this rule on WSL. // Instead, everything is executable. Therefore, we skip this rule on WSL.
if wsl::is_wsl() { if wsl::is_wsl() {
return None; return None;
} }
let ShebangDirective { offset, contents } = shebang;
if let Ok(false) = is_executable(filepath) { if let Ok(false) = is_executable(filepath) {
let diagnostic = Diagnostic::new( return Some(Diagnostic::new(ShebangNotExecutable, range));
ShebangNotExecutable,
TextRange::at(range.start() + offset, contents.text_len()),
);
return Some(diagnostic);
} }
None None
} }
#[cfg(not(target_family = "unix"))] #[cfg(not(target_family = "unix"))]
pub(crate) fn shebang_not_executable( pub(crate) fn shebang_not_executable(_filepath: &Path, _range: TextRange) -> Option<Diagnostic> {
_filepath: &Path,
_range: TextRange,
_shebang: &ShebangDirective,
) -> Option<Diagnostic> {
None None
} }

View file

@ -1,9 +1,9 @@
use ruff_text_size::{TextLen, TextRange}; use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::source_code::Locator;
use crate::comments::shebang::ShebangDirective; use ruff_python_trivia::is_python_whitespace;
/// ## What it does /// ## What it does
/// Checks for a shebang directive that is not at the beginning of the file. /// Checks for a shebang directive that is not at the beginning of the file.
@ -42,19 +42,20 @@ impl Violation for ShebangNotFirstLine {
} }
/// EXE005 /// EXE005
pub(crate) fn shebang_newline( pub(crate) fn shebang_not_first_line(range: TextRange, locator: &Locator) -> Option<Diagnostic> {
range: TextRange, // If the shebang is at the beginning of the file, abort.
shebang: &ShebangDirective, if range.start() == TextSize::from(0) {
first_line: bool, return None;
) -> Option<Diagnostic> {
let ShebangDirective { offset, contents } = shebang;
if first_line {
None
} else {
Some(Diagnostic::new(
ShebangNotFirstLine,
TextRange::at(range.start() + offset, contents.text_len()),
))
} }
// 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))
} }

View file

@ -1,10 +1,10 @@
--- ---
source: crates/ruff/src/rules/flake8_executable/mod.rs 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 1 | #!/usr/bin/python
| ^^^^^^^^^^^^^^^ EXE001 | ^^^^^^^^^^^^^^^^^ EXE001
2 | 2 |
3 | if __name__ == '__main__': 3 | if __name__ == '__main__':
| |

View file

@ -1,11 +1,10 @@
--- ---
source: crates/ruff/src/rules/flake8_executable/mod.rs 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 2 | pass #!/usr/bin/env python
| ^^^^^^^^^^^^^^^^^^^^^ EXE005
| |

View file

@ -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

View file

@ -1,11 +1,11 @@
--- ---
source: crates/ruff/src/rules/flake8_executable/mod.rs 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 2 | # A python comment
3 | #!/usr/bin/python 3 | #!/usr/bin/python
| ^^^^^^^^^^^^^^^ EXE005 | ^^^^^^^^^^^^^^^^^ EXE005
| |

View file

@ -1,11 +1,11 @@
--- ---
source: crates/ruff/src/rules/flake8_executable/mod.rs 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 3 | # A python comment
4 | #!/usr/bin/python 4 | #!/usr/bin/python
| ^^^^^^^^^^^^^^^ EXE005 | ^^^^^^^^^^^^^^^^^ EXE005
| |

View file

@ -1,12 +1,12 @@
--- ---
source: crates/ruff/src/rules/flake8_executable/mod.rs 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 | """ 4 | """
5 | # A python comment 5 | # A python comment
6 | #!/usr/bin/python 6 | #!/usr/bin/python
| ^^^^^^^^^^^^^^^ EXE005 | ^^^^^^^^^^^^^^^^^ EXE005
| |