diff --git a/LICENSE b/LICENSE index 9de3bd6e59cb8..77cb3f108f449 100644 --- a/LICENSE +++ b/LICENSE @@ -471,6 +471,29 @@ are: SOFTWARE. """ +- pygrep-hooks, licensed as follows: + """ + Copyright (c) 2018 Anthony Sottile + + 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. + """ + - pyupgrade, licensed as follows: """ Copyright (c) 2017 Anthony Sottile diff --git a/README.md b/README.md index 403655bd7b56d..1a4931cde9cb3 100644 --- a/README.md +++ b/README.md @@ -77,18 +77,20 @@ of [Conda](https://docs.conda.io/en/latest/): 1. [pep8-naming (N)](#pep8-naming) 1. [eradicate (ERA)](#eradicate) 1. [flake8-bandit (S)](#flake8-bandit) - 1. [flake8-comprehensions (C)](#flake8-comprehensions) + 1. [flake8-comprehensions (C4)](#flake8-comprehensions) 1. [flake8-boolean-trap (FBT)](#flake8-boolean-trap) 1. [flake8-bugbear (B)](#flake8-bugbear) 1. [flake8-builtins (A)](#flake8-builtins) - 1. [flake8-debugger (T)](#flake8-debugger) + 1. [flake8-debugger (T10)](#flake8-debugger) 1. [flake8-tidy-imports (I25)](#flake8-tidy-imports) - 1. [flake8-print (T)](#flake8-print) + 1. [flake8-print (T20)](#flake8-print) 1. [flake8-quotes (Q)](#flake8-quotes) 1. [flake8-annotations (ANN)](#flake8-annotations) 1. [flake8-2020 (YTT)](#flake8-2020) 1. [flake8-blind-except (BLE)](#flake8-blind-except) 1. [mccabe (C90)](#mccabe) + 1. [pygrep-hooks (PGH)](#pygrep-hooks) + 1. [Pylint (PL)](#pylint) 1. [Ruff-specific rules (RUF)](#ruff-specific-rules) 1. [Meta rules (M)](#meta-rules) 1. [Editor Integrations](#editor-integrations) @@ -726,6 +728,14 @@ For more, see [mccabe](https://pypi.org/project/mccabe/0.7.0/) on PyPI. | ---- | ---- | ------- | --- | | C901 | FunctionIsTooComplex | `...` is too complex (10) | | +### pygrep-hooks + +For more, see [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks) on GitHub. + +| Code | Name | Message | Fix | +| ---- | ---- | ------- | --- | +| PGH001 | NoEval | No builtin `eval()` allowed | | + ### Pylint For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI. @@ -902,6 +912,7 @@ natively, including: - [`yesqa`](https://github.com/asottile/yesqa) - [`eradicate`](https://pypi.org/project/eradicate/) - [`pyupgrade`](https://pypi.org/project/pyupgrade/) (16/33) +- [`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (1/10) - [`autoflake`](https://pypi.org/project/autoflake/) (1/7) Beyond the rule set, Ruff suffers from the following limitations vis-à-vis Flake8: @@ -946,8 +957,10 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl - [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/) (1/3) - [`mccabe`](https://pypi.org/project/mccabe/) -Ruff can also replace [`isort`](https://pypi.org/project/isort/), [`yesqa`](https://github.com/asottile/yesqa), -and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (16/33). +Ruff can also replace [`isort`](https://pypi.org/project/isort/), +[`yesqa`](https://github.com/asottile/yesqa), [`eradicate`](https://pypi.org/project/eradicate/), +[`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (1/10), and a subset of the rules +implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (16/33). If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue. @@ -1261,7 +1274,7 @@ Exclusions are based on globs, and can be either: (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`). -Note that you'll typically want to use [`extend_exclude`](#extend_exclude) to modify the excluded +Note that you'll typically want to use [`extend_exclude`](#extend-exclude) to modify the excluded paths. **Default value**: `[".bzr", ".direnv", ".eggs", ".git", ".hg", ".mypy_cache", ".nox", ".pants.d", ".ruff_cache", ".svn", ".tox", ".venv", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "venv"]` diff --git a/resources/test/fixtures/pygrep-hooks/PGH001_0.py b/resources/test/fixtures/pygrep-hooks/PGH001_0.py new file mode 100644 index 0000000000000..eed83b81f987c --- /dev/null +++ b/resources/test/fixtures/pygrep-hooks/PGH001_0.py @@ -0,0 +1,9 @@ +from ast import literal_eval + +eval("3 + 4") + +literal_eval({1: 2}) + + +def fn() -> None: + eval("3 + 4") diff --git a/resources/test/fixtures/pygrep-hooks/PGH001_1.py b/resources/test/fixtures/pygrep-hooks/PGH001_1.py new file mode 100644 index 0000000000000..ecb3e91a3a5d5 --- /dev/null +++ b/resources/test/fixtures/pygrep-hooks/PGH001_1.py @@ -0,0 +1,11 @@ +def eval(content: str) -> None: + pass + + +eval("3 + 4") + +literal_eval({1: 2}) + + +def fn() -> None: + eval("3 + 4") diff --git a/ruff_dev/src/generate_rules_table.rs b/ruff_dev/src/generate_rules_table.rs index 02eccc294c29c..a5d186f52c643 100644 --- a/ruff_dev/src/generate_rules_table.rs +++ b/ruff_dev/src/generate_rules_table.rs @@ -28,11 +28,12 @@ pub fn main(cli: &Cli) -> Result<()> { output.push('\n'); output.push('\n'); - if let Some(url) = check_category.url() { + if let Some((url, platform)) = check_category.url() { output.push_str(&format!( - "For more, see [{}]({}) on PyPI.", + "For more, see [{}]({}) on {}.", check_category.title(), - url + url, + platform )); output.push('\n'); output.push('\n'); diff --git a/src/check_ast.rs b/src/check_ast.rs index 7331b167189c1..b123c17847060 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -37,7 +37,7 @@ use crate::{ docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_debugger, flake8_print, flake8_tidy_imports, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, - pylint, pyupgrade, rules, + pygrep_hooks, pylint, pyupgrade, rules, }; const GLOBAL_SCOPE_INDEX: usize = 0; @@ -1700,6 +1700,11 @@ where } } + // pygrep-hooks + if self.settings.enabled.contains(&CheckCode::PGH001) { + pygrep_hooks::checks::no_eval(self, func); + } + // Ruff if self.settings.enabled.contains(&CheckCode::RUF101) { rules::plugins::convert_exit_to_sys_exit(self, func); diff --git a/src/checks.rs b/src/checks.rs index 2c2a98f708d01..3d0e867415c66 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -278,6 +278,8 @@ pub enum CheckCode { RUF101, // Meta M001, + // pygrep-hooks + PGH001, } #[derive(EnumIter, Debug, PartialEq, Eq)] @@ -302,11 +304,26 @@ pub enum CheckCategory { Flake82020, Flake8BlindExcept, McCabe, + PygrepHooks, Pylint, Ruff, Meta, } +pub enum Platform { + PyPI, + GitHub, +} + +impl fmt::Display for Platform { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Platform::PyPI => fmt.write_str("PyPI"), + Platform::GitHub => fmt.write_str("GitHub"), + } + } +} + impl CheckCategory { pub fn title(&self) -> &'static str { match self { @@ -331,51 +348,97 @@ impl CheckCategory { CheckCategory::Pydocstyle => "pydocstyle", CheckCategory::Pyflakes => "Pyflakes", CheckCategory::Pylint => "Pylint", + CheckCategory::PygrepHooks => "pygrep-hooks", CheckCategory::Pyupgrade => "pyupgrade", CheckCategory::Ruff => "Ruff-specific rules", } } - pub fn url(&self) -> Option<&'static str> { + pub fn url(&self) -> Option<(&'static str, &'static Platform)> { match self { - CheckCategory::Eradicate => Some("https://pypi.org/project/eradicate/2.1.0/"), - CheckCategory::Flake82020 => Some("https://pypi.org/project/flake8-2020/1.7.0/"), - CheckCategory::Flake8Annotations => { - Some("https://pypi.org/project/flake8-annotations/2.9.1/") + CheckCategory::Eradicate => { + Some(("https://pypi.org/project/eradicate/2.1.0/", &Platform::PyPI)) + } + CheckCategory::Flake82020 => Some(( + "https://pypi.org/project/flake8-2020/1.7.0/", + &Platform::PyPI, + )), + CheckCategory::Flake8Annotations => Some(( + "https://pypi.org/project/flake8-annotations/2.9.1/", + &Platform::PyPI, + )), + CheckCategory::Flake8Bandit => Some(( + "https://pypi.org/project/flake8-bandit/4.1.1/", + &Platform::PyPI, + )), + CheckCategory::Flake8BlindExcept => Some(( + "https://pypi.org/project/flake8-blind-except/0.2.1/", + &Platform::PyPI, + )), + CheckCategory::Flake8BooleanTrap => Some(( + "https://pypi.org/project/flake8-boolean-trap/0.1.0/", + &Platform::PyPI, + )), + CheckCategory::Flake8Bugbear => Some(( + "https://pypi.org/project/flake8-bugbear/22.10.27/", + &Platform::PyPI, + )), + CheckCategory::Flake8Builtins => Some(( + "https://pypi.org/project/flake8-builtins/2.0.1/", + &Platform::PyPI, + )), + CheckCategory::Flake8Comprehensions => Some(( + "https://pypi.org/project/flake8-comprehensions/3.10.1/", + &Platform::PyPI, + )), + CheckCategory::Flake8Debugger => Some(( + "https://pypi.org/project/flake8-debugger/4.1.2/", + &Platform::PyPI, + )), + CheckCategory::Flake8Print => Some(( + "https://pypi.org/project/flake8-print/5.0.0/", + &Platform::PyPI, + )), + CheckCategory::Flake8Quotes => Some(( + "https://pypi.org/project/flake8-quotes/3.3.1/", + &Platform::PyPI, + )), + CheckCategory::Flake8TidyImports => Some(( + "https://pypi.org/project/flake8-tidy-imports/4.8.0/", + &Platform::PyPI, + )), + CheckCategory::Isort => { + Some(("https://pypi.org/project/isort/5.10.1/", &Platform::PyPI)) + } + CheckCategory::McCabe => { + Some(("https://pypi.org/project/mccabe/0.7.0/", &Platform::PyPI)) } - CheckCategory::Flake8Bandit => Some("https://pypi.org/project/flake8-bandit/4.1.1/"), - CheckCategory::Flake8BlindExcept => { - Some("https://pypi.org/project/flake8-blind-except/0.2.1/") - } - CheckCategory::Flake8BooleanTrap => { - Some("https://pypi.org/project/flake8-boolean-trap/0.1.0/") - } - CheckCategory::Flake8Bugbear => { - Some("https://pypi.org/project/flake8-bugbear/22.10.27/") - } - CheckCategory::Flake8Builtins => { - Some("https://pypi.org/project/flake8-builtins/2.0.1/") - } - CheckCategory::Flake8Comprehensions => { - Some("https://pypi.org/project/flake8-comprehensions/3.10.1/") - } - CheckCategory::Flake8Debugger => { - Some("https://pypi.org/project/flake8-debugger/4.1.2/") - } - CheckCategory::Flake8Print => Some("https://pypi.org/project/flake8-print/5.0.0/"), - CheckCategory::Flake8Quotes => Some("https://pypi.org/project/flake8-quotes/3.3.1/"), - CheckCategory::Flake8TidyImports => { - Some("https://pypi.org/project/flake8-tidy-imports/4.8.0/") - } - CheckCategory::Isort => Some("https://pypi.org/project/isort/5.10.1/"), - CheckCategory::McCabe => Some("https://pypi.org/project/mccabe/0.7.0/"), CheckCategory::Meta => None, - CheckCategory::PEP8Naming => Some("https://pypi.org/project/pep8-naming/0.13.2/"), - CheckCategory::Pycodestyle => Some("https://pypi.org/project/pycodestyle/2.9.1/"), - CheckCategory::Pydocstyle => Some("https://pypi.org/project/pydocstyle/6.1.1/"), - CheckCategory::Pyflakes => Some("https://pypi.org/project/pyflakes/2.5.0/"), - CheckCategory::Pylint => Some("https://pypi.org/project/pylint/2.15.7/"), - CheckCategory::Pyupgrade => Some("https://pypi.org/project/pyupgrade/3.2.0/"), + CheckCategory::PEP8Naming => Some(( + "https://pypi.org/project/pep8-naming/0.13.2/", + &Platform::PyPI, + )), + CheckCategory::Pycodestyle => Some(( + "https://pypi.org/project/pycodestyle/2.9.1/", + &Platform::PyPI, + )), + CheckCategory::Pydocstyle => Some(( + "https://pypi.org/project/pydocstyle/6.1.1/", + &Platform::PyPI, + )), + CheckCategory::Pyflakes => { + Some(("https://pypi.org/project/pyflakes/2.5.0/", &Platform::PyPI)) + } + CheckCategory::Pylint => { + Some(("https://pypi.org/project/pylint/2.15.7/", &Platform::PyPI)) + } + CheckCategory::PygrepHooks => Some(( + "https://github.com/pre-commit/pygrep-hooks", + &Platform::GitHub, + )), + CheckCategory::Pyupgrade => { + Some(("https://pypi.org/project/pyupgrade/3.2.0/", &Platform::PyPI)) + } CheckCategory::Ruff => None, } } @@ -657,6 +720,8 @@ pub enum CheckKind { BooleanPositionalArgInFunctionDefinition, BooleanDefaultValueInFunctionDefinition, BooleanPositionalValueInFunctionCall, + // pygrep-hooks + NoEval, // Ruff AmbiguousUnicodeCharacterString(char, char), AmbiguousUnicodeCharacterDocstring(char, char), @@ -975,6 +1040,8 @@ impl CheckCode { CheckCode::FBT001 => CheckKind::BooleanPositionalArgInFunctionDefinition, CheckCode::FBT002 => CheckKind::BooleanDefaultValueInFunctionDefinition, CheckCode::FBT003 => CheckKind::BooleanPositionalValueInFunctionCall, + // pygrep-hooks + CheckCode::PGH001 => CheckKind::NoEval, // Ruff CheckCode::RUF001 => CheckKind::AmbiguousUnicodeCharacterString('𝐁', 'B'), CheckCode::RUF002 => CheckKind::AmbiguousUnicodeCharacterDocstring('𝐁', 'B'), @@ -1169,6 +1236,7 @@ impl CheckCode { CheckCode::N816 => CheckCategory::PEP8Naming, CheckCode::N817 => CheckCategory::PEP8Naming, CheckCode::N818 => CheckCategory::PEP8Naming, + CheckCode::PGH001 => CheckCategory::PygrepHooks, CheckCode::PLE1142 => CheckCategory::Pylint, CheckCode::Q000 => CheckCategory::Flake8Quotes, CheckCode::Q001 => CheckCategory::Flake8Quotes, @@ -1456,12 +1524,14 @@ impl CheckKind { CheckKind::HardcodedPasswordString(..) => &CheckCode::S105, CheckKind::HardcodedPasswordFuncArg(..) => &CheckCode::S106, CheckKind::HardcodedPasswordDefault(..) => &CheckCode::S107, - // McCabe + // mccabe CheckKind::FunctionIsTooComplex(..) => &CheckCode::C901, // flake8-boolean-trap CheckKind::BooleanPositionalArgInFunctionDefinition => &CheckCode::FBT001, CheckKind::BooleanDefaultValueInFunctionDefinition => &CheckCode::FBT002, CheckKind::BooleanPositionalValueInFunctionCall => &CheckCode::FBT003, + // pygrep-hooks + CheckKind::NoEval => &CheckCode::PGH001, // Ruff CheckKind::AmbiguousUnicodeCharacterString(..) => &CheckCode::RUF001, CheckKind::AmbiguousUnicodeCharacterDocstring(..) => &CheckCode::RUF002, @@ -2183,7 +2253,7 @@ impl CheckKind { } // flake8-blind-except CheckKind::BlindExcept => "Blind except Exception: statement".to_string(), - // McCabe + // mccabe CheckKind::FunctionIsTooComplex(name, complexity) => { format!("`{name}` is too complex ({complexity})") } @@ -2197,6 +2267,8 @@ impl CheckKind { CheckKind::BooleanPositionalValueInFunctionCall => { "Boolean positional value in function call".to_string() } + // pygrep-hooks + CheckKind::NoEval => "No builtin `eval()` allowed".to_string(), // Ruff CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => { format!( diff --git a/src/checks_gen.rs b/src/checks_gen.rs index efe3723d53c25..9768cfee0bb1e 100644 --- a/src/checks_gen.rs +++ b/src/checks_gen.rs @@ -278,6 +278,10 @@ pub enum CheckCodePrefix { N816, N817, N818, + PGH, + PGH0, + PGH00, + PGH001, PLE, PLE1, PLE11, @@ -1148,6 +1152,10 @@ impl CheckCodePrefix { CheckCodePrefix::N816 => vec![CheckCode::N816], CheckCodePrefix::N817 => vec![CheckCode::N817], CheckCodePrefix::N818 => vec![CheckCode::N818], + CheckCodePrefix::PGH => vec![CheckCode::PGH001], + CheckCodePrefix::PGH0 => vec![CheckCode::PGH001], + CheckCodePrefix::PGH00 => vec![CheckCode::PGH001], + CheckCodePrefix::PGH001 => vec![CheckCode::PGH001], CheckCodePrefix::PLE => vec![CheckCode::PLE1142], CheckCodePrefix::PLE1 => vec![CheckCode::PLE1142], CheckCodePrefix::PLE11 => vec![CheckCode::PLE1142], @@ -1615,6 +1623,10 @@ impl CheckCodePrefix { CheckCodePrefix::N816 => SuffixLength::Three, CheckCodePrefix::N817 => SuffixLength::Three, CheckCodePrefix::N818 => SuffixLength::Three, + CheckCodePrefix::PGH => SuffixLength::Zero, + CheckCodePrefix::PGH0 => SuffixLength::One, + CheckCodePrefix::PGH00 => SuffixLength::Two, + CheckCodePrefix::PGH001 => SuffixLength::Three, CheckCodePrefix::PLE => SuffixLength::Zero, CheckCodePrefix::PLE1 => SuffixLength::One, CheckCodePrefix::PLE11 => SuffixLength::Two, @@ -1713,6 +1725,7 @@ pub const CATEGORIES: &[CheckCodePrefix] = &[ CheckCodePrefix::I, CheckCodePrefix::M, CheckCodePrefix::N, + CheckCodePrefix::PGH, CheckCodePrefix::PLE, CheckCodePrefix::Q, CheckCodePrefix::RUF, diff --git a/src/lib.rs b/src/lib.rs index db2cc9cc6494f..fcb9552bf5feb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,6 +65,7 @@ pub mod printer; mod pycodestyle; mod pydocstyle; mod pyflakes; +mod pygrep_hooks; mod pylint; mod python; mod pyupgrade; diff --git a/src/pygrep_hooks/checks.rs b/src/pygrep_hooks/checks.rs new file mode 100644 index 0000000000000..c2a32d2b07da6 --- /dev/null +++ b/src/pygrep_hooks/checks.rs @@ -0,0 +1,15 @@ +use rustpython_ast::{Expr, ExprKind}; + +use crate::ast::types::Range; +use crate::check_ast::Checker; +use crate::checks::{Check, CheckKind}; + +pub fn no_eval(checker: &mut Checker, func: &Expr) { + if let ExprKind::Name { id, .. } = &func.node { + if id == "eval" { + if checker.is_builtin("eval") { + checker.add_check(Check::new(CheckKind::NoEval, Range::from_located(func))); + } + } + } +} diff --git a/src/pygrep_hooks/mod.rs b/src/pygrep_hooks/mod.rs new file mode 100644 index 0000000000000..5955c25d0574b --- /dev/null +++ b/src/pygrep_hooks/mod.rs @@ -0,0 +1,30 @@ +pub mod checks; + +#[cfg(test)] +mod tests { + use std::convert::AsRef; + use std::path::Path; + + use anyhow::Result; + use test_case::test_case; + + use crate::checks::CheckCode; + use crate::linter::test_path; + use crate::settings; + + #[test_case(CheckCode::PGH001, Path::new("PGH001_0.py"); "PGH001_0")] + #[test_case(CheckCode::PGH001, Path::new("PGH001_1.py"); "PGH001_1")] + fn checks(check_code: CheckCode, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy()); + let mut checks = test_path( + Path::new("./resources/test/fixtures/pygrep-hooks") + .join(path) + .as_path(), + &settings::Settings::for_rule(check_code), + true, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(snapshot, checks); + Ok(()) + } +} diff --git a/src/pygrep_hooks/snapshots/ruff__pygrep_hooks__tests__PGH001_PGH001_0.py.snap b/src/pygrep_hooks/snapshots/ruff__pygrep_hooks__tests__PGH001_PGH001_0.py.snap new file mode 100644 index 0000000000000..7f05ee7e7d76d --- /dev/null +++ b/src/pygrep_hooks/snapshots/ruff__pygrep_hooks__tests__PGH001_PGH001_0.py.snap @@ -0,0 +1,21 @@ +--- +source: src/pygrep_hooks/mod.rs +expression: checks +--- +- kind: NoEval + location: + row: 3 + column: 0 + end_location: + row: 3 + column: 4 + fix: ~ +- kind: NoEval + location: + row: 9 + column: 4 + end_location: + row: 9 + column: 8 + fix: ~ + diff --git a/src/pygrep_hooks/snapshots/ruff__pygrep_hooks__tests__PGH001_PGH001_1.py.snap b/src/pygrep_hooks/snapshots/ruff__pygrep_hooks__tests__PGH001_PGH001_1.py.snap new file mode 100644 index 0000000000000..68aa191216b4e --- /dev/null +++ b/src/pygrep_hooks/snapshots/ruff__pygrep_hooks__tests__PGH001_PGH001_1.py.snap @@ -0,0 +1,6 @@ +--- +source: src/pygrep_hooks/mod.rs +expression: checks +--- +[] +