diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0164ee8a9..3b31cae9bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ There are four phases to adding a new lint rule: 1. Define the violation struct in `src/violations.rs` (e.g., `ModuleImportNotAtTopOfFile`). 2. Map the violation struct to a rule code in `src/registry.rs` (e.g., `E402`). 3. Define the logic for triggering the violation in `src/checkers/ast.rs` (for AST-based checks), - `src/checkers/tokens.rs` (for token-based checks), or `src/checkers/lines.rs` (for text-based checks). + `src/checkers/tokens.rs` (for token-based checks), `src/checkers/lines.rs` (for text-based checks) or `src/checkers/filesystem.rs` (for filesystem-based checks). 4. Add a test fixture. 5. Update the generated files (documentation and generated code). diff --git a/README.md b/README.md index cc0d66952e..ed26be42d0 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ developer of [Zulip](https://github.com/zulip/zulip): 1. [Pylint (PLC, PLE, PLR, PLW)](#pylint-plc-ple-plr-plw) 1. [flake8-pie (PIE)](#flake8-pie-pie) 1. [flake8-commas (COM)](#flake8-commas-com) + 1. [flake8-no-pep420 (INP)](#flake8-no-pep420-inp) 1. [Ruff-specific rules (RUF)](#ruff-specific-rules-ruf) 1. [Editor Integrations](#editor-integrations) 1. [FAQ](#faq) @@ -1158,6 +1159,14 @@ For more, see [flake8-commas](https://pypi.org/project/flake8-commas/2.1.0/) on | COM818 | TrailingCommaOnBareTupleProhibited | Trailing comma on bare tuple prohibited | | | COM819 | TrailingCommaProhibited | Trailing comma prohibited | 🛠 | +### flake8-no-pep420 (INP) + +For more, see [flake8-no-pep420](https://pypi.org/project/flake8-boolean-trap/2.3.0/) on PyPI. + +| Code | Name | Message | Fix | +| ---- | ---- | ------- | --- | +| INP001 | ImplicitNamespacePackage | File `...` is part of an implicit namespace package. Add an `__init__.py`. | | + ### Ruff-specific rules (RUF) | Code | Name | Message | Fix | @@ -1451,6 +1460,7 @@ natively, including: - [`flake8-errmsg`](https://pypi.org/project/flake8-errmsg/) - [`flake8-implicit-str-concat`](https://pypi.org/project/flake8-implicit-str-concat/) - [`flake8-import-conventions`](https://github.com/joaopalmeiro/flake8-import-conventions) +- [`flake8-no-pep420`](https://pypi.org/project/flake8-no-pep420) - [`flake8-pie`](https://pypi.org/project/flake8-pie/) ([#1543](https://github.com/charliermarsh/ruff/issues/1543)) - [`flake8-print`](https://pypi.org/project/flake8-print/) - [`flake8-quotes`](https://pypi.org/project/flake8-quotes/) @@ -1518,6 +1528,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl - [`flake8-errmsg`](https://pypi.org/project/flake8-errmsg/) - [`flake8-implicit-str-concat`](https://pypi.org/project/flake8-implicit-str-concat/) - [`flake8-import-conventions`](https://github.com/joaopalmeiro/flake8-import-conventions) +- [`flake8-no-pep420`](https://pypi.org/project/flake8-no-pep420) - [`flake8-pie`](https://pypi.org/project/flake8-pie/) ([#1543](https://github.com/charliermarsh/ruff/issues/1543)) - [`flake8-print`](https://pypi.org/project/flake8-print/) - [`flake8-quotes`](https://pypi.org/project/flake8-quotes/) diff --git a/licenses/LICENSE_adamchainz_flake8-no-pep420 b/licenses/LICENSE_adamchainz_flake8-no-pep420 new file mode 100644 index 0000000000..2f2f8df2fd --- /dev/null +++ b/licenses/LICENSE_adamchainz_flake8-no-pep420 @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Adam Johnson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/test/fixtures/flake8_no_pep420/test_fail_empty/example.py b/resources/test/fixtures/flake8_no_pep420/test_fail_empty/example.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/test/fixtures/flake8_no_pep420/test_fail_nonempty/example.py b/resources/test/fixtures/flake8_no_pep420/test_fail_nonempty/example.py new file mode 100644 index 0000000000..9f1b437537 --- /dev/null +++ b/resources/test/fixtures/flake8_no_pep420/test_fail_nonempty/example.py @@ -0,0 +1 @@ +print('hi') diff --git a/resources/test/fixtures/flake8_no_pep420/test_fail_shebang/example.py b/resources/test/fixtures/flake8_no_pep420/test_fail_shebang/example.py new file mode 100644 index 0000000000..290621873b --- /dev/null +++ b/resources/test/fixtures/flake8_no_pep420/test_fail_shebang/example.py @@ -0,0 +1,2 @@ +#!/bin/env/python +print('hi') diff --git a/resources/test/fixtures/flake8_no_pep420/test_ignored/example.py b/resources/test/fixtures/flake8_no_pep420/test_ignored/example.py new file mode 100644 index 0000000000..237b8b488b --- /dev/null +++ b/resources/test/fixtures/flake8_no_pep420/test_ignored/example.py @@ -0,0 +1 @@ +import os # noqa: INP001 diff --git a/resources/test/fixtures/flake8_no_pep420/test_pass/__init__.py b/resources/test/fixtures/flake8_no_pep420/test_pass/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/test/fixtures/flake8_no_pep420/test_pass/example.py b/resources/test/fixtures/flake8_no_pep420/test_pass/example.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ruff.schema.json b/ruff.schema.json index beef56bd41..655114b43c 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1356,6 +1356,10 @@ "ICN0", "ICN00", "ICN001", + "INP", + "INP0", + "INP00", + "INP001", "ISC", "ISC0", "ISC00", diff --git a/src/checkers/filesystem.rs b/src/checkers/filesystem.rs new file mode 100644 index 0000000000..f6798c90d8 --- /dev/null +++ b/src/checkers/filesystem.rs @@ -0,0 +1,18 @@ +use std::path::Path; + +use crate::registry::{Diagnostic, RuleCode}; +use crate::rules::flake8_no_pep420::rules::implicit_namespace_package; +use crate::settings::Settings; + +pub fn check_file_path(path: &Path, settings: &Settings) -> Vec { + let mut diagnostics: Vec = vec![]; + + // flake8-no-pep420 + if settings.rules.enabled(&RuleCode::INP001) { + if let Some(diagnostic) = implicit_namespace_package(path) { + diagnostics.push(diagnostic); + } + } + + diagnostics +} diff --git a/src/checkers/mod.rs b/src/checkers/mod.rs index c792dc4169..cec54e376b 100644 --- a/src/checkers/mod.rs +++ b/src/checkers/mod.rs @@ -1,4 +1,5 @@ pub mod ast; +pub mod filesystem; pub mod imports; pub mod lines; pub mod noqa; diff --git a/src/linter.rs b/src/linter.rs index ea0cc849cd..9c880c0491 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -7,6 +7,7 @@ use rustpython_parser::lexer::LexResult; use crate::ast::types::Range; use crate::autofix::fix_file; use crate::checkers::ast::check_ast; +use crate::checkers::filesystem::check_file_path; use crate::checkers::imports::check_imports; use crate::checkers::lines::check_lines; use crate::checkers::noqa::check_noqa; @@ -62,6 +63,15 @@ pub fn check_path( diagnostics.extend(check_tokens(locator, &tokens, settings, autofix)); } + // Run the filesystem-based rules. + if settings + .rules + .iter_enabled() + .any(|rule_code| matches!(rule_code.lint_source(), LintSource::Filesystem)) + { + diagnostics.extend(check_file_path(path, settings)); + } + // Run the AST-based rules. let use_ast = settings .rules diff --git a/src/registry.rs b/src/registry.rs index 93d6db273e..a9bb57d67f 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -416,6 +416,8 @@ ruff_macros::define_rule_mapping!( COM812 => violations::TrailingCommaMissing, COM818 => violations::TrailingCommaOnBareTupleProhibited, COM819 => violations::TrailingCommaProhibited, + // flake8-no-pep420 + INP001 => violations::ImplicitNamespacePackage, // Ruff RUF001 => violations::AmbiguousUnicodeCharacterString, RUF002 => violations::AmbiguousUnicodeCharacterDocstring, @@ -459,6 +461,7 @@ pub enum RuleOrigin { Pylint, Flake8Pie, Flake8Commas, + Flake8NoPep420, Ruff, } @@ -525,6 +528,7 @@ impl RuleOrigin { RuleOrigin::Pyupgrade => Prefixes::Single(RuleCodePrefix::UP), RuleOrigin::Flake8Pie => Prefixes::Single(RuleCodePrefix::PIE), RuleOrigin::Flake8Commas => Prefixes::Single(RuleCodePrefix::COM), + RuleOrigin::Flake8NoPep420 => Prefixes::Single(RuleCodePrefix::INP), RuleOrigin::Ruff => Prefixes::Single(RuleCodePrefix::RUF), } } @@ -537,6 +541,7 @@ pub enum LintSource { Tokens, Imports, NoQa, + Filesystem, } impl RuleCode { @@ -567,6 +572,7 @@ impl RuleCode { | RuleCode::RUF003 => &LintSource::Tokens, RuleCode::E902 => &LintSource::Io, RuleCode::I001 | RuleCode::I002 => &LintSource::Imports, + RuleCode::INP001 => &LintSource::Filesystem, _ => &LintSource::Ast, } } diff --git a/src/rules/flake8_no_pep420/mod.rs b/src/rules/flake8_no_pep420/mod.rs new file mode 100644 index 0000000000..e6ab58ba0d --- /dev/null +++ b/src/rules/flake8_no_pep420/mod.rs @@ -0,0 +1,32 @@ +//! Rules from [flake8-no-pep420](https://pypi.org/project/flake8-boolean-trap/2.3.0/). +pub(crate) mod rules; + +#[cfg(test)] +mod tests { + use std::path::Path; + + use anyhow::Result; + use test_case::test_case; + + use crate::linter::test_path; + use crate::registry::RuleCode; + use crate::settings::Settings; + + #[test_case(Path::new("test_pass"); "INP001_0")] + #[test_case(Path::new("test_fail_empty"); "INP001_1")] + #[test_case(Path::new("test_fail_nonempty"); "INP001_2")] + #[test_case(Path::new("test_fail_shebang"); "INP001_3")] + #[test_case(Path::new("test_ignored"); "INP001_4")] + fn test_flake8_no_pep420(path: &Path) -> Result<()> { + let snapshot = format!("{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("./resources/test/fixtures/flake8_no_pep420") + .join(path) + .join("example.py") + .as_path(), + &Settings::for_rule(RuleCode::INP001), + )?; + insta::assert_yaml_snapshot!(snapshot, diagnostics); + Ok(()) + } +} diff --git a/src/rules/flake8_no_pep420/rules.rs b/src/rules/flake8_no_pep420/rules.rs new file mode 100644 index 0000000000..2294619932 --- /dev/null +++ b/src/rules/flake8_no_pep420/rules.rs @@ -0,0 +1,18 @@ +use std::path::Path; + +use crate::ast::types::Range; +use crate::registry::Diagnostic; +use crate::violations; + +/// INP001 +pub fn implicit_namespace_package(path: &Path) -> Option { + if let Some(parent) = path.parent() { + if !parent.join("__init__.py").as_path().exists() { + return Some(Diagnostic::new( + violations::ImplicitNamespacePackage(path.to_string_lossy().to_string()), + Range::default(), + )); + } + } + None +} diff --git a/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_fail_empty.snap b/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_fail_empty.snap new file mode 100644 index 0000000000..3e45d9cd19 --- /dev/null +++ b/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_fail_empty.snap @@ -0,0 +1,15 @@ +--- +source: src/rules/flake8_no_pep420/mod.rs +expression: diagnostics +--- +- kind: + ImplicitNamespacePackage: "./resources/test/fixtures/flake8_no_pep420/test_fail_empty/example.py" + location: + row: 1 + column: 0 + end_location: + row: 1 + column: 0 + fix: ~ + parent: ~ + diff --git a/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_fail_nonempty.snap b/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_fail_nonempty.snap new file mode 100644 index 0000000000..9f08942058 --- /dev/null +++ b/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_fail_nonempty.snap @@ -0,0 +1,15 @@ +--- +source: src/rules/flake8_no_pep420/mod.rs +expression: diagnostics +--- +- kind: + ImplicitNamespacePackage: "./resources/test/fixtures/flake8_no_pep420/test_fail_nonempty/example.py" + location: + row: 1 + column: 0 + end_location: + row: 1 + column: 0 + fix: ~ + parent: ~ + diff --git a/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_fail_shebang.snap b/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_fail_shebang.snap new file mode 100644 index 0000000000..c00c75380b --- /dev/null +++ b/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_fail_shebang.snap @@ -0,0 +1,15 @@ +--- +source: src/rules/flake8_no_pep420/mod.rs +expression: diagnostics +--- +- kind: + ImplicitNamespacePackage: "./resources/test/fixtures/flake8_no_pep420/test_fail_shebang/example.py" + location: + row: 1 + column: 0 + end_location: + row: 1 + column: 0 + fix: ~ + parent: ~ + diff --git a/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_ignored.snap b/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_ignored.snap new file mode 100644 index 0000000000..622ebcff04 --- /dev/null +++ b/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_ignored.snap @@ -0,0 +1,6 @@ +--- +source: src/rules/flake8_no_pep420/mod.rs +expression: diagnostics +--- +[] + diff --git a/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_pass.snap b/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_pass.snap new file mode 100644 index 0000000000..622ebcff04 --- /dev/null +++ b/src/rules/flake8_no_pep420/snapshots/ruff__rules__flake8_no_pep420__tests__test_pass.snap @@ -0,0 +1,6 @@ +--- +source: src/rules/flake8_no_pep420/mod.rs +expression: diagnostics +--- +[] + diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 98aea59129..d3c424f21d 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -13,6 +13,7 @@ pub mod flake8_debugger; pub mod flake8_errmsg; pub mod flake8_implicit_str_concat; pub mod flake8_import_conventions; +pub mod flake8_no_pep420; pub mod flake8_pie; pub mod flake8_print; pub mod flake8_pytest_style; diff --git a/src/violations.rs b/src/violations.rs index ee57946a42..9ee414c20b 100644 --- a/src/violations.rs +++ b/src/violations.rs @@ -6088,6 +6088,22 @@ impl AlwaysAutofixableViolation for TrailingCommaProhibited { } } +// flake8-no-pep420 + +define_violation!( + pub struct ImplicitNamespacePackage(pub String); +); +impl Violation for ImplicitNamespacePackage { + fn message(&self) -> String { + let ImplicitNamespacePackage(filename) = self; + format!("File `{filename}` is part of an implicit namespace package. Add an `__init__.py`.") + } + + fn placeholder() -> Self { + ImplicitNamespacePackage("...".to_string()) + } +} + // Ruff define_violation!(