diff --git a/README.md b/README.md index 3824d7a906..5a2fa1e9e8 100644 --- a/README.md +++ b/README.md @@ -1188,6 +1188,8 @@ For more, see [flake8-executable](https://pypi.org/project/flake8-executable/) o | Code | Name | Message | Fix | | ---- | ---- | ------- | --- | +| EXE001 | shebang-not-executable | Shebang is present but file is not executable | | +| EXE002 | shebang-missing-executable-file | The file is executable but no shebang is present | | | EXE003 | shebang-python | Shebang should contain "python" | | | EXE004 | shebang-whitespace | Avoid whitespace before shebang | 🛠 | | EXE005 | shebang-newline | Shebang should be at the beginning of the file | | diff --git a/resources/test/fixtures/flake8_executable/EXE001_1.py b/resources/test/fixtures/flake8_executable/EXE001_1.py new file mode 100644 index 0000000000..6e9bbae579 --- /dev/null +++ b/resources/test/fixtures/flake8_executable/EXE001_1.py @@ -0,0 +1,4 @@ +#!/usr/bin/python + +if __name__ == '__main__': + print('I should be executable.') diff --git a/resources/test/fixtures/flake8_executable/EXE001_2.py b/resources/test/fixtures/flake8_executable/EXE001_2.py new file mode 100644 index 0000000000..21c3d83e41 --- /dev/null +++ b/resources/test/fixtures/flake8_executable/EXE001_2.py @@ -0,0 +1,2 @@ +if __name__ == '__main__': + print('I should be executable.') diff --git a/resources/test/fixtures/flake8_executable/EXE001_3.py b/resources/test/fixtures/flake8_executable/EXE001_3.py new file mode 100755 index 0000000000..6e9bbae579 --- /dev/null +++ b/resources/test/fixtures/flake8_executable/EXE001_3.py @@ -0,0 +1,4 @@ +#!/usr/bin/python + +if __name__ == '__main__': + print('I should be executable.') diff --git a/resources/test/fixtures/flake8_executable/EXE002_1.py b/resources/test/fixtures/flake8_executable/EXE002_1.py new file mode 100755 index 0000000000..21c3d83e41 --- /dev/null +++ b/resources/test/fixtures/flake8_executable/EXE002_1.py @@ -0,0 +1,2 @@ +if __name__ == '__main__': + print('I should be executable.') diff --git a/resources/test/fixtures/flake8_executable/EXE002_2.py b/resources/test/fixtures/flake8_executable/EXE002_2.py new file mode 100644 index 0000000000..21c3d83e41 --- /dev/null +++ b/resources/test/fixtures/flake8_executable/EXE002_2.py @@ -0,0 +1,2 @@ +if __name__ == '__main__': + print('I should be executable.') diff --git a/resources/test/fixtures/flake8_executable/EXE002_3.py b/resources/test/fixtures/flake8_executable/EXE002_3.py new file mode 100755 index 0000000000..6e9bbae579 --- /dev/null +++ b/resources/test/fixtures/flake8_executable/EXE002_3.py @@ -0,0 +1,4 @@ +#!/usr/bin/python + +if __name__ == '__main__': + print('I should be executable.') diff --git a/resources/test/fixtures/flake8_executable/EXE003.py b/resources/test/fixtures/flake8_executable/EXE003.py old mode 100644 new mode 100755 diff --git a/resources/test/fixtures/flake8_executable/EXE004_1.py b/resources/test/fixtures/flake8_executable/EXE004_1.py old mode 100644 new mode 100755 diff --git a/resources/test/fixtures/flake8_executable/EXE004_3.py b/resources/test/fixtures/flake8_executable/EXE004_3.py old mode 100644 new mode 100755 diff --git a/resources/test/fixtures/flake8_executable/EXE005_1.py b/resources/test/fixtures/flake8_executable/EXE005_1.py old mode 100644 new mode 100755 diff --git a/resources/test/fixtures/flake8_executable/EXE005_2.py b/resources/test/fixtures/flake8_executable/EXE005_2.py old mode 100644 new mode 100755 diff --git a/resources/test/fixtures/flake8_executable/EXE005_3.py b/resources/test/fixtures/flake8_executable/EXE005_3.py old mode 100644 new mode 100755 diff --git a/ruff.schema.json b/ruff.schema.json index 48c1be3b19..cfaba7b93f 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1405,6 +1405,8 @@ "EXE", "EXE0", "EXE00", + "EXE001", + "EXE002", "EXE003", "EXE004", "EXE005", diff --git a/src/checkers/lines.rs b/src/checkers/lines.rs index 4c579eaa26..4c3efd2aeb 100644 --- a/src/checkers/lines.rs +++ b/src/checkers/lines.rs @@ -1,8 +1,12 @@ //! Lint rules based on checking raw physical lines. +use std::path::Path; + use crate::registry::{Diagnostic, Rule}; -use crate::rules::flake8_executable::helpers::extract_shebang; -use crate::rules::flake8_executable::rules::{shebang_newline, shebang_python, shebang_whitespace}; +use crate::rules::flake8_executable::helpers::{extract_shebang, ShebangDirective}; +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, }; @@ -11,6 +15,7 @@ use crate::rules::pyupgrade::rules::unnecessary_coding_comment; use crate::settings::{flags, Settings}; pub fn check_lines( + path: &Path, contents: &str, commented_lines: &[usize], doc_lines: &[usize], @@ -18,8 +23,11 @@ pub fn check_lines( autofix: flags::Autofix, ) -> Vec { let mut diagnostics: Vec = vec![]; + let mut has_any_shebang = false; let enforce_blanket_noqa = settings.rules.enabled(&Rule::BlanketNOQA); + 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::ShebangWhitespace); let enforce_shebang_newline = settings.rules.enabled(&Rule::ShebangNewline); let enforce_shebang_python = settings.rules.enabled(&Rule::ShebangPython); @@ -68,8 +76,23 @@ pub fn check_lines( } } - if enforce_shebang_whitespace || enforce_shebang_newline || enforce_shebang_python { + if enforce_shebang_missing + || enforce_shebang_not_executable + || enforce_shebang_whitespace + || enforce_shebang_newline + || enforce_shebang_python + { let shebang = extract_shebang(line); + if enforce_shebang_not_executable { + if let Some(diagnostic) = shebang_not_executable(path, index, &shebang) { + diagnostics.push(diagnostic); + } + } + if enforce_shebang_missing { + if !has_any_shebang && matches!(shebang, ShebangDirective::Match(_, _, _, _)) { + has_any_shebang = true; + } + } if enforce_shebang_whitespace { if let Some(diagnostic) = shebang_whitespace(index, &shebang, fix_shebang_whitespace) @@ -124,12 +147,20 @@ pub fn check_lines( } } + if enforce_shebang_missing && !has_any_shebang { + if let Some(diagnostic) = shebang_missing(path) { + diagnostics.push(diagnostic); + } + } + diagnostics } #[cfg(test)] mod tests { + use std::path::Path; + use super::check_lines; use crate::registry::Rule; use crate::settings::{flags, Settings}; @@ -139,6 +170,7 @@ mod tests { let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8. let check_with_max_line_length = |line_length: usize| { check_lines( + Path::new("foo.py"), line, &[], &[], diff --git a/src/linter.rs b/src/linter.rs index 6b8b40845e..2a2bb9d3bd 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -141,6 +141,7 @@ pub fn check_path( .any(|rule_code| matches!(rule_code.lint_source(), LintSource::Lines)) { diagnostics.extend(check_lines( + path, contents, indexer.commented_lines(), &doc_lines, diff --git a/src/registry.rs b/src/registry.rs index f472b15a40..b9d0b69642 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -424,6 +424,8 @@ ruff_macros::define_rule_mapping!( // flake8-no-pep420 INP001 => violations::ImplicitNamespacePackage, // flake8-executable + EXE001 => rules::flake8_executable::rules::ShebangNotExecutable, + EXE002 => rules::flake8_executable::rules::ShebangMissingExecutableFile, EXE003 => rules::flake8_executable::rules::ShebangPython, EXE004 => rules::flake8_executable::rules::ShebangWhitespace, EXE005 => rules::flake8_executable::rules::ShebangNewline, @@ -646,6 +648,8 @@ impl Rule { | Rule::MixedSpacesAndTabs | Rule::NoNewLineAtEndOfFile | Rule::PEP3120UnnecessaryCodingComment + | Rule::ShebangMissingExecutableFile + | Rule::ShebangNotExecutable | Rule::ShebangNewline | Rule::ShebangPython | Rule::ShebangWhitespace => &LintSource::Lines, diff --git a/src/rules/flake8_executable/mod.rs b/src/rules/flake8_executable/mod.rs index 379e3a0041..7a78c67aaa 100644 --- a/src/rules/flake8_executable/mod.rs +++ b/src/rules/flake8_executable/mod.rs @@ -13,6 +13,12 @@ mod tests { use crate::registry::Rule; use crate::settings; + #[test_case(Path::new("EXE001_1.py"); "EXE001_1")] + #[test_case(Path::new("EXE001_2.py"); "EXE001_2")] + #[test_case(Path::new("EXE001_3.py"); "EXE001_3")] + #[test_case(Path::new("EXE002_1.py"); "EXE002_1")] + #[test_case(Path::new("EXE002_2.py"); "EXE002_2")] + #[test_case(Path::new("EXE002_3.py"); "EXE002_3")] #[test_case(Path::new("EXE003.py"); "EXE003")] #[test_case(Path::new("EXE004_1.py"); "EXE004_1")] #[test_case(Path::new("EXE004_2.py"); "EXE004_2")] @@ -27,6 +33,8 @@ mod tests { .join(path) .as_path(), &settings::Settings::for_rules(vec![ + Rule::ShebangNotExecutable, + Rule::ShebangMissingExecutableFile, Rule::ShebangWhitespace, Rule::ShebangNewline, Rule::ShebangPython, diff --git a/src/rules/flake8_executable/rules/mod.rs b/src/rules/flake8_executable/rules/mod.rs index c333772e2b..d3359ba611 100644 --- a/src/rules/flake8_executable/rules/mod.rs +++ b/src/rules/flake8_executable/rules/mod.rs @@ -1,7 +1,11 @@ +pub use shebang_missing::{shebang_missing, ShebangMissingExecutableFile}; pub use shebang_newline::{shebang_newline, ShebangNewline}; +pub use shebang_not_executable::{shebang_not_executable, ShebangNotExecutable}; pub use shebang_python::{shebang_python, ShebangPython}; pub use shebang_whitespace::{shebang_whitespace, ShebangWhitespace}; +mod shebang_missing; mod shebang_newline; +mod shebang_not_executable; mod shebang_python; mod shebang_whitespace; diff --git a/src/rules/flake8_executable/rules/shebang_missing.rs b/src/rules/flake8_executable/rules/shebang_missing.rs new file mode 100644 index 0000000000..5233085274 --- /dev/null +++ b/src/rules/flake8_executable/rules/shebang_missing.rs @@ -0,0 +1,42 @@ +#[cfg(not(target_family = "wasm"))] +use std::os::unix::prelude::MetadataExt; +use std::path::Path; + +use ruff_macros::derive_message_formats; + +#[cfg(not(target_family = "wasm"))] +use crate::ast::types::Range; +use crate::define_violation; +use crate::registry::Diagnostic; +use crate::violation::Violation; + +define_violation!( + pub struct ShebangMissingExecutableFile; +); +impl Violation for ShebangMissingExecutableFile { + #[derive_message_formats] + fn message(&self) -> String { + format!("The file is executable but no shebang is present") + } +} + +/// EXE002 +#[cfg(not(target_family = "wasm"))] +pub fn shebang_missing(filepath: &Path) -> Option { + if let Ok(metadata) = filepath.metadata() { + // Check if file is executable by anyone + if metadata.mode() & 0o111 == 0 { + None + } else { + let diagnostic = Diagnostic::new(ShebangMissingExecutableFile, Range::default()); + Some(diagnostic) + } + } else { + None + } +} + +#[cfg(target_family = "wasm")] +pub fn shebang_missing(_filepath: &Path) -> Option { + None +} diff --git a/src/rules/flake8_executable/rules/shebang_not_executable.rs b/src/rules/flake8_executable/rules/shebang_not_executable.rs new file mode 100644 index 0000000000..85e5976bbb --- /dev/null +++ b/src/rules/flake8_executable/rules/shebang_not_executable.rs @@ -0,0 +1,63 @@ +#[cfg(not(target_family = "wasm"))] +use std::os::unix::prelude::MetadataExt; +use std::path::Path; + +use ruff_macros::derive_message_formats; +#[cfg(not(target_family = "wasm"))] +use rustpython_ast::Location; + +#[cfg(not(target_family = "wasm"))] +use crate::ast::types::Range; +use crate::define_violation; +use crate::registry::Diagnostic; +use crate::rules::flake8_executable::helpers::ShebangDirective; +use crate::violation::Violation; + +define_violation!( + pub struct ShebangNotExecutable; +); +impl Violation for ShebangNotExecutable { + #[derive_message_formats] + fn message(&self) -> String { + format!("Shebang is present but file is not executable") + } +} + +/// EXE001 +#[cfg(not(target_family = "wasm"))] +pub fn shebang_not_executable( + filepath: &Path, + lineno: usize, + shebang: &ShebangDirective, +) -> Option { + if let ShebangDirective::Match(_, start, end, _) = shebang { + if let Ok(metadata) = filepath.metadata() { + // Check if file is executable by anyone + if metadata.mode() & 0o111 == 0 { + let diagnostic = Diagnostic::new( + ShebangNotExecutable, + Range::new( + Location::new(lineno + 1, *start), + Location::new(lineno + 1, *end), + ), + ); + Some(diagnostic) + } else { + None + } + } else { + None + } + } else { + None + } +} + +#[cfg(target_family = "wasm")] +pub fn shebang_not_executable( + _filepath: &Path, + _lineno: usize, + _shebang: &ShebangDirective, +) -> Option { + None +} diff --git a/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_1.py.snap b/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_1.py.snap new file mode 100644 index 0000000000..7fe4020fdb --- /dev/null +++ b/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_1.py.snap @@ -0,0 +1,15 @@ +--- +source: src/rules/flake8_executable/mod.rs +expression: diagnostics +--- +- kind: + ShebangNotExecutable: ~ + location: + row: 1 + column: 2 + end_location: + row: 1 + column: 17 + fix: ~ + parent: ~ + diff --git a/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_2.py.snap b/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_2.py.snap new file mode 100644 index 0000000000..c738409c4f --- /dev/null +++ b/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_2.py.snap @@ -0,0 +1,6 @@ +--- +source: src/rules/flake8_executable/mod.rs +expression: diagnostics +--- +[] + diff --git a/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_3.py.snap b/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_3.py.snap new file mode 100644 index 0000000000..c738409c4f --- /dev/null +++ b/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE001_3.py.snap @@ -0,0 +1,6 @@ +--- +source: src/rules/flake8_executable/mod.rs +expression: diagnostics +--- +[] + diff --git a/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE002_1.py.snap b/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE002_1.py.snap new file mode 100644 index 0000000000..8874a32d9c --- /dev/null +++ b/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE002_1.py.snap @@ -0,0 +1,15 @@ +--- +source: src/rules/flake8_executable/mod.rs +expression: diagnostics +--- +- kind: + ShebangMissingExecutableFile: ~ + location: + row: 1 + column: 0 + end_location: + row: 1 + column: 0 + fix: ~ + parent: ~ + diff --git a/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE002_2.py.snap b/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE002_2.py.snap new file mode 100644 index 0000000000..c738409c4f --- /dev/null +++ b/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE002_2.py.snap @@ -0,0 +1,6 @@ +--- +source: src/rules/flake8_executable/mod.rs +expression: diagnostics +--- +[] + diff --git a/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE002_3.py.snap b/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE002_3.py.snap new file mode 100644 index 0000000000..c738409c4f --- /dev/null +++ b/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE002_3.py.snap @@ -0,0 +1,6 @@ +--- +source: src/rules/flake8_executable/mod.rs +expression: diagnostics +--- +[] +