diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index 007f02a003..92524ef2f0 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -7,9 +7,9 @@ use ruff_diagnostics::Diagnostic; use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; use ruff_python_whitespace::UniversalNewlines; +use crate::comments::shebang::ShebangDirective; use crate::registry::Rule; use crate::rules::flake8_copyright::rules::missing_copyright_notice; -use crate::rules::flake8_executable::helpers::ShebangDirective; use crate::rules::flake8_executable::rules::{ shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace, }; diff --git a/crates/ruff/src/comments/mod.rs b/crates/ruff/src/comments/mod.rs new file mode 100644 index 0000000000..4b88bac2ed --- /dev/null +++ b/crates/ruff/src/comments/mod.rs @@ -0,0 +1 @@ +pub(crate) mod shebang; diff --git a/crates/ruff/src/comments/shebang.rs b/crates/ruff/src/comments/shebang.rs new file mode 100644 index 0000000000..a9a6bb13b1 --- /dev/null +++ b/crates/ruff/src/comments/shebang.rs @@ -0,0 +1,67 @@ +use ruff_python_whitespace::{is_python_whitespace, Cursor}; +use ruff_text_size::{TextLen, TextSize}; + +/// 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, +} + +impl<'a> ShebangDirective<'a> { + /// Parse a shebang directive from a line, or return `None` if the line does not contain a + /// shebang directive. + 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; + } + if !cursor.eat_char('!') { + return None; + } + + Some(Self { + offset: line.text_len() - cursor.text_len(), + contents: cursor.chars().as_str(), + }) + } +} + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + + use super::ShebangDirective; + + #[test] + fn shebang_non_match() { + let source = "not a match"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } + + #[test] + fn shebang_end_of_line() { + let source = "print('test') #!/usr/bin/python"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } + + #[test] + fn shebang_match() { + let source = "#!/usr/bin/env python"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } + + #[test] + fn shebang_leading_space() { + let source = " #!/usr/bin/env python"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } +} diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_end_of_line.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_end_of_line.snap similarity index 52% rename from crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_end_of_line.snap rename to crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_end_of_line.snap index e5550fcc2c..87d72c9d88 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_end_of_line.snap +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_end_of_line.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff/src/rules/flake8_executable/helpers.rs +source: crates/ruff/src/comments/shebang.rs expression: "ShebangDirective::try_extract(source)" --- None diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_leading_space.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap similarity index 72% rename from crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_leading_space.snap rename to crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap index abb2535298..8ea8bfcfca 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_leading_space.snap +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff/src/rules/flake8_executable/helpers.rs +source: crates/ruff/src/comments/shebang.rs expression: "ShebangDirective::try_extract(source)" --- Some( diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_match.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap similarity index 72% rename from crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_match.snap rename to crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap index 05f3d5fe3b..c0ec6ca308 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_match.snap +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff/src/rules/flake8_executable/helpers.rs +source: crates/ruff/src/comments/shebang.rs expression: "ShebangDirective::try_extract(source)" --- Some( diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_non_match.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_non_match.snap similarity index 52% rename from crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_non_match.snap rename to crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_non_match.snap index e5550fcc2c..87d72c9d88 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__helpers__tests__shebang_non_match.snap +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_non_match.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff/src/rules/flake8_executable/helpers.rs +source: crates/ruff/src/comments/shebang.rs expression: "ShebangDirective::try_extract(source)" --- None diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index b224744c31..0f65d8ffbf 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -14,6 +14,7 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); mod autofix; mod checkers; mod codes; +mod comments; mod cst; pub mod directives; mod doc_lines; diff --git a/crates/ruff/src/rules/flake8_executable/helpers.rs b/crates/ruff/src/rules/flake8_executable/helpers.rs index 7f23613bc5..be200dfb2c 100644 --- a/crates/ruff/src/rules/flake8_executable/helpers.rs +++ b/crates/ruff/src/rules/flake8_executable/helpers.rs @@ -1,92 +1,12 @@ -#[cfg(target_family = "unix")] +#![cfg(target_family = "unix")] + use std::os::unix::fs::PermissionsExt; -#[cfg(target_family = "unix")] use std::path::Path; -#[cfg(target_family = "unix")] use anyhow::Result; -use ruff_text_size::{TextLen, TextSize}; -/// 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, -} - -impl<'a> ShebangDirective<'a> { - /// Parse a shebang directive from a line, or return `None` if the line does not contain a - /// shebang directive. - pub(crate) fn try_extract(line: &'a str) -> Option { - // Trim whitespace. - let directive = Self::lex_whitespace(line); - - // Trim the `#!` prefix. - let directive = Self::lex_char(directive, '#')?; - let directive = Self::lex_char(directive, '!')?; - - Some(Self { - offset: line.text_len() - directive.text_len(), - contents: directive, - }) - } - - /// Lex optional leading whitespace. - #[inline] - fn lex_whitespace(line: &str) -> &str { - line.trim_start() - } - - /// Lex a specific character, or return `None` if the character is not the first character in - /// the line. - #[inline] - fn lex_char(line: &str, c: char) -> Option<&str> { - let mut chars = line.chars(); - if chars.next() == Some(c) { - Some(chars.as_str()) - } else { - None - } - } -} - -#[cfg(target_family = "unix")] pub(super) fn is_executable(filepath: &Path) -> Result { let metadata = filepath.metadata()?; let permissions = metadata.permissions(); Ok(permissions.mode() & 0o111 != 0) } - -#[cfg(test)] -mod tests { - use insta::assert_debug_snapshot; - - use crate::rules::flake8_executable::helpers::ShebangDirective; - - #[test] - fn shebang_non_match() { - let source = "not a match"; - assert_debug_snapshot!(ShebangDirective::try_extract(source)); - } - - #[test] - fn shebang_end_of_line() { - let source = "print('test') #!/usr/bin/python"; - assert_debug_snapshot!(ShebangDirective::try_extract(source)); - } - - #[test] - fn shebang_match() { - let source = "#!/usr/bin/env python"; - assert_debug_snapshot!(ShebangDirective::try_extract(source)); - } - - #[test] - fn shebang_leading_space() { - let source = " #!/usr/bin/env python"; - assert_debug_snapshot!(ShebangDirective::try_extract(source)); - } -} diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs index d1ae29ee77..626e29020a 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs @@ -3,7 +3,7 @@ use ruff_text_size::{TextLen, TextRange}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use crate::rules::flake8_executable::helpers::ShebangDirective; +use crate::comments::shebang::ShebangDirective; /// ## What it does /// Checks for a shebang directive that is not at the beginning of the file. 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 b5b37037fc..56ec4b3249 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 @@ -7,10 +7,10 @@ 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; -use crate::rules::flake8_executable::helpers::ShebangDirective; /// ## What it does /// Checks for a shebang directive in a file that is not executable. diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs index 0552de6f05..832533d8ca 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs @@ -3,7 +3,7 @@ use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use crate::rules::flake8_executable::helpers::ShebangDirective; +use crate::comments::shebang::ShebangDirective; /// ## What it does /// Checks for a shebang directive in `.py` files that does not contain `python`. diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs index b0394344a8..3b8c990897 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs @@ -1,10 +1,11 @@ -use ruff_text_size::{TextRange, TextSize}; 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 crate::rules::flake8_executable::helpers::ShebangDirective; +use crate::comments::shebang::ShebangDirective; /// ## What it does /// Checks for whitespace before a shebang directive.