Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
#!/usr/bin/env -S uv run
print("hello world")

#!/usr/bin/env uv --offline run
print("offline")

#!/usr/bin/env uv --color=auto run
print("color")

#!/usr/bin/env uv --quiet run --script
print("quiet run script")

#!/usr/bin/env uv tool run
print("uv tool")

#!/usr/bin/env uvx
print("uvx")

#!/usr/bin/env uvx --quiet
print("uvx quiet")

#!/usr/bin/env uv_not_really_run
print("this should fail")
44 changes: 19 additions & 25 deletions crates/ruff_linter/src/rules/flake8_executable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,24 @@ mod tests {
use crate::test::test_path;
use crate::{assert_diagnostics, settings};

#[test_case(Path::new("EXE001_1.py"))]
#[test_case(Path::new("EXE001_2.py"))]
#[test_case(Path::new("EXE001_3.py"))]
#[test_case(Path::new("EXE002_1.py"))]
#[test_case(Path::new("EXE002_2.py"))]
#[test_case(Path::new("EXE002_3.py"))]
#[test_case(Path::new("EXE003.py"))]
#[test_case(Path::new("EXE003_uv.py"))]
#[test_case(Path::new("EXE003_uv_tool.py"))]
#[test_case(Path::new("EXE003_uvx.py"))]
#[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"))]
fn rules(path: &Path) -> Result<()> {
#[test_case(Rule::ShebangNotExecutable, Path::new("EXE001_1.py"))]
#[test_case(Rule::ShebangNotExecutable, Path::new("EXE001_2.py"))]
#[test_case(Rule::ShebangNotExecutable, Path::new("EXE001_3.py"))]
#[test_case(Rule::ShebangMissingExecutableFile, Path::new("EXE002_1.py"))]
#[test_case(Rule::ShebangMissingExecutableFile, Path::new("EXE002_2.py"))]
#[test_case(Rule::ShebangMissingExecutableFile, Path::new("EXE002_3.py"))]
#[test_case(Rule::ShebangMissingPython, Path::new("EXE003.py"))]
#[test_case(Rule::ShebangMissingPython, Path::new("EXE003_uv.py"))]
#[test_case(Rule::ShebangMissingPython, Path::new("EXE003_uv_tool.py"))]
#[test_case(Rule::ShebangMissingPython, Path::new("EXE003_uvx.py"))]
#[test_case(Rule::ShebangLeadingWhitespace, Path::new("EXE004_1.py"))]
#[test_case(Rule::ShebangLeadingWhitespace, Path::new("EXE004_2.py"))]
#[test_case(Rule::ShebangLeadingWhitespace, Path::new("EXE004_3.py"))]
#[test_case(Rule::ShebangLeadingWhitespace, Path::new("EXE004_4.py"))]
#[test_case(Rule::ShebangNotFirstLine, Path::new("EXE005_1.py"))]
#[test_case(Rule::ShebangNotFirstLine, Path::new("EXE005_2.py"))]
#[test_case(Rule::ShebangNotFirstLine, Path::new("EXE005_3.py"))]
fn rules(rule: Rule, path: &Path) -> Result<()> {
if is_wsl::is_wsl() {
// these rules are always ignored on WSL, so skip testing them in a WSL environment
// see https://github.com/astral-sh/ruff/pull/21724 for latest discussion
Expand All @@ -41,13 +41,7 @@ mod tests {
let snapshot = path.to_string_lossy().into_owned();
let diagnostics = test_path(
Path::new("flake8_executable").join(path).as_path(),
&settings::LinterSettings::for_rules(vec![
Rule::ShebangNotExecutable,
Rule::ShebangMissingExecutableFile,
Rule::ShebangLeadingWhitespace,
Rule::ShebangNotFirstLine,
Rule::ShebangMissingPython,
]),
&settings::LinterSettings::for_rule(rule),
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
use ruff_text_size::TextRange;
use std::sync::LazyLock;

use regex::Regex;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_text_size::TextRange;

use crate::Violation;
use crate::checkers::ast::LintContext;
use crate::comments::shebang::ShebangDirective;

static UV_RUN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?x)
\b
(?:
# Part A: uv or uv tool (these MUST be followed by run)
(?:uv|uv\s+tool) \s+ (?:--?[a-zA-Z][\w-]*(?:[=\s]\S+)?\s+)* run
|
# Part B: uvx (stands alone, run is optional/redundant)
uvx (?: \s+ .* )?
)
\b
",
)
.unwrap()
});

/// ## What it does
/// Checks for a shebang directive in `.py` files that does not contain `python`,
/// `pytest`, or `uv run`.
Expand Down Expand Up @@ -48,12 +67,7 @@ pub(crate) fn shebang_missing_python(
shebang: &ShebangDirective,
context: &LintContext,
) {
if shebang.contains("python")
|| shebang.contains("pytest")
|| shebang.contains("uv run")
|| shebang.contains("uvx")
|| shebang.contains("uv tool run")
{
if shebang.contains("python") || shebang.contains("pytest") || UV_RUN_REGEX.is_match(shebang) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
---
source: crates/ruff_linter/src/rules/flake8_executable/mod.rs
---

EXE003 Shebang should contain `python`, `pytest`, or `uv run`
--> EXE003_uv.py:22:1
|
20 | print("uvx quiet")
21 |
22 | #!/usr/bin/env uv_not_really_run
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | print("this should fail")
|
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_executable/mod.rs
---
EXE005 Shebang should be at the beginning of the file
--> EXE004_3.py:2:7
|
2 | pass #!/usr/bin/env python
| ^^^^^^^^^^^^^^^^^^^^^
|