From 7c1a6bce7beedef7119495b8354a7524de8d1dab Mon Sep 17 00:00:00 2001
From: Maksudul Haque <saad.mk112@gmail.com>
Date: Wed, 1 Feb 2023 05:09:40 +0600
Subject: [PATCH] [`flake8-raise`] Add Plugin and `RSE102` Rule (#2354)

---
 LICENSE                                       | 25 +++++++++++++++
 README.md                                     | 11 +++++++
 .../test/fixtures/flake8_raise/RSE102.py      | 15 +++++++++
 ruff.schema.json                              |  4 +++
 src/checkers/ast.rs                           | 11 ++++++-
 src/registry.rs                               |  5 +++
 src/rules/flake8_raise/mod.rs                 | 28 +++++++++++++++++
 src/rules/flake8_raise/rules/mod.rs           |  5 +++
 .../unnecessary_paren_on_raise_exception.rs   | 31 +++++++++++++++++++
 ...ry-paren-on-raise-exception_RSE102.py.snap | 25 +++++++++++++++
 src/rules/mod.rs                              |  1 +
 11 files changed, 160 insertions(+), 1 deletion(-)
 create mode 100644 resources/test/fixtures/flake8_raise/RSE102.py
 create mode 100644 src/rules/flake8_raise/mod.rs
 create mode 100644 src/rules/flake8_raise/rules/mod.rs
 create mode 100644 src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs
 create mode 100644 src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap

diff --git a/LICENSE b/LICENSE
index 7811d42465347d..d7dbadbd38591c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1005,3 +1005,28 @@ are:
     See the License for the specific language governing permissions and
     limitations under the License.
   """
+
+- flake8-raise, licensed as follows:
+  """
+    MIT License
+
+    Copyright (c) 2020 Jon Dufresne
+
+    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/README.md b/README.md
index d83845dd90ffc2..a4fd8ceb80d366 100644
--- a/README.md
+++ b/README.md
@@ -158,6 +158,7 @@ developer of [Zulip](https://github.com/zulip/zulip):
    1. [pygrep-hooks (PGH)](#pygrep-hooks-pgh)
    1. [Pylint (PL)](#pylint-pl)
    1. [tryceratops (TRY)](#tryceratops-try)
+   1. [flake8-raise (RSE)](#flake8-raise-rse)
    1. [Ruff-specific rules (RUF)](#ruff-specific-rules-ruf)<!-- End auto-generated table of contents. -->
 1. [Editor Integrations](#editor-integrations)
 1. [FAQ](#faq)
@@ -1357,6 +1358,14 @@ For more, see [tryceratops](https://pypi.org/project/tryceratops/1.1.0/) on PyPI
 | TRY301 | raise-within-try | Abstract `raise` to an inner function |  |
 | TRY400 | error-instead-of-exception | Use `logging.exception` instead of `logging.error` |  |
 
+### flake8-raise (RSE)
+
+For more, see [flake8-raise](https://pypi.org/project/flake8-raise/) on PyPI.
+
+| Code | Name | Message | Fix |
+| ---- | ---- | ------- | --- |
+| RSE102 | unnecessary-paren-on-raise-exception | Unnecessary parentheses on raised exception |  |
+
 ### Ruff-specific rules (RUF)
 
 | Code | Name | Message | Fix |
@@ -1648,6 +1657,7 @@ natively, including:
 - [`flake8-print`](https://pypi.org/project/flake8-print/)
 - [`flake8-pytest-style`](https://pypi.org/project/flake8-pytest-style/)
 - [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
+- [`flake8-raise`](https://pypi.org/project/flake8-raise/)
 - [`flake8-return`](https://pypi.org/project/flake8-return/)
 - [`flake8-simplify`](https://pypi.org/project/flake8-simplify/) ([#998](https://github.com/charliermarsh/ruff/issues/998))
 - [`flake8-super`](https://pypi.org/project/flake8-super/)
@@ -1736,6 +1746,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
 - [`flake8-print`](https://pypi.org/project/flake8-print/)
 - [`flake8-pytest-style`](https://pypi.org/project/flake8-pytest-style/)
 - [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
+- [`flake8-raise`](https://pypi.org/project/flake8-raise/)
 - [`flake8-return`](https://pypi.org/project/flake8-return/)
 - [`flake8-simplify`](https://pypi.org/project/flake8-simplify/) ([#998](https://github.com/charliermarsh/ruff/issues/998))
 - [`flake8-super`](https://pypi.org/project/flake8-super/)
diff --git a/resources/test/fixtures/flake8_raise/RSE102.py b/resources/test/fixtures/flake8_raise/RSE102.py
new file mode 100644
index 00000000000000..ba377a856b2e17
--- /dev/null
+++ b/resources/test/fixtures/flake8_raise/RSE102.py
@@ -0,0 +1,15 @@
+try:
+    y = 6 + "7"
+except TypeError:
+    raise ValueError()  # RSE102
+
+try:
+    x = 1 / 0
+except ZeroDivisionError:
+    raise
+
+raise TypeError()  # RSE102
+
+raise AssertionError
+
+raise AttributeError("test message")
diff --git a/ruff.schema.json b/ruff.schema.json
index 16f6986bd08d5e..f8e532a1b99426 100644
--- a/ruff.schema.json
+++ b/ruff.schema.json
@@ -1739,6 +1739,10 @@
         "RET506",
         "RET507",
         "RET508",
+        "RSE",
+        "RSE1",
+        "RSE10",
+        "RSE102",
         "RUF",
         "RUF0",
         "RUF00",
diff --git a/src/checkers/ast.rs b/src/checkers/ast.rs
index 8f2bd34e794885..b885deba7322d6 100644
--- a/src/checkers/ast.rs
+++ b/src/checkers/ast.rs
@@ -36,7 +36,7 @@ use crate::rules::{
     flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
     flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
     flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format,
-    flake8_pie, flake8_print, flake8_pytest_style, flake8_return, flake8_simplify,
+    flake8_pie, flake8_print, flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify,
     flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, flake8_use_pathlib, mccabe,
     pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade,
     ruff, tryceratops,
@@ -1434,6 +1434,15 @@ where
                         tryceratops::rules::raise_vanilla_args(self, expr);
                     }
                 }
+                if self
+                    .settings
+                    .rules
+                    .enabled(&Rule::UnnecessaryParenOnRaiseException)
+                {
+                    if let Some(expr) = exc {
+                        flake8_raise::rules::unnecessary_paren_on_raise_exception(self, expr);
+                    }
+                }
             }
             StmtKind::AugAssign { target, .. } => {
                 self.handle_node_load(target);
diff --git a/src/registry.rs b/src/registry.rs
index fd7f9484ee2352..3f656eec1283c9 100644
--- a/src/registry.rs
+++ b/src/registry.rs
@@ -481,6 +481,8 @@ ruff_macros::define_rule_mapping!(
     G101 => rules::flake8_logging_format::violations::LoggingExtraAttrClash,
     G201 => rules::flake8_logging_format::violations::LoggingExcInfo,
     G202 => rules::flake8_logging_format::violations::LoggingRedundantExcInfo,
+    // flake8-raise
+    RSE102 => rules::flake8_raise::rules::UnnecessaryParenOnRaiseException,
     // ruff
     RUF001 => violations::AmbiguousUnicodeCharacterString,
     RUF002 => violations::AmbiguousUnicodeCharacterDocstring,
@@ -610,6 +612,9 @@ pub enum Linter {
     /// [tryceratops](https://pypi.org/project/tryceratops/1.1.0/)
     #[prefix = "TRY"]
     Tryceratops,
+    /// [flake8-raise](https://pypi.org/project/flake8-raise/)
+    #[prefix = "RSE"]
+    Flake8Raise,
     /// Ruff-specific rules
     #[prefix = "RUF"]
     Ruff,
diff --git a/src/rules/flake8_raise/mod.rs b/src/rules/flake8_raise/mod.rs
new file mode 100644
index 00000000000000..21345e25c77140
--- /dev/null
+++ b/src/rules/flake8_raise/mod.rs
@@ -0,0 +1,28 @@
+//! Rules from [flake8-raise](https://pypi.org/project/flake8-raise/).
+pub(crate) mod rules;
+
+#[cfg(test)]
+mod tests {
+    use std::convert::AsRef;
+    use std::path::Path;
+
+    use anyhow::Result;
+    use test_case::test_case;
+
+    use crate::linter::test_path;
+    use crate::registry::Rule;
+    use crate::{assert_yaml_snapshot, settings};
+
+    #[test_case(Rule::UnnecessaryParenOnRaiseException, Path::new("RSE102.py"); "RSE102")]
+    fn rules(rule_code: Rule, path: &Path) -> Result<()> {
+        let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
+        let diagnostics = test_path(
+            Path::new("./resources/test/fixtures/flake8_raise")
+                .join(path)
+                .as_path(),
+            &settings::Settings::for_rule(rule_code),
+        )?;
+        assert_yaml_snapshot!(snapshot, diagnostics);
+        Ok(())
+    }
+}
diff --git a/src/rules/flake8_raise/rules/mod.rs b/src/rules/flake8_raise/rules/mod.rs
new file mode 100644
index 00000000000000..7177469472841a
--- /dev/null
+++ b/src/rules/flake8_raise/rules/mod.rs
@@ -0,0 +1,5 @@
+pub use unnecessary_paren_on_raise_exception::{
+    unnecessary_paren_on_raise_exception, UnnecessaryParenOnRaiseException,
+};
+
+mod unnecessary_paren_on_raise_exception;
diff --git a/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs b/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs
new file mode 100644
index 00000000000000..40f26e98475c8c
--- /dev/null
+++ b/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs
@@ -0,0 +1,31 @@
+use ruff_macros::derive_message_formats;
+
+use crate::ast::types::Range;
+use crate::checkers::ast::Checker;
+use crate::define_violation;
+use crate::registry::Diagnostic;
+use crate::violation::Violation;
+use rustpython_ast::{Expr, ExprKind};
+
+define_violation!(
+    pub struct UnnecessaryParenOnRaiseException;
+);
+impl Violation for UnnecessaryParenOnRaiseException {
+    #[derive_message_formats]
+    fn message(&self) -> String {
+        format!("Unnecessary parentheses on raised exception")
+    }
+}
+
+/// RSE102
+pub fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr: &Expr) {
+    match &expr.node {
+        ExprKind::Call { args, keywords, .. } if args.is_empty() && keywords.is_empty() => {
+            checker.diagnostics.push(Diagnostic::new(
+                UnnecessaryParenOnRaiseException,
+                Range::from_located(expr),
+            ));
+        }
+        _ => (),
+    }
+}
diff --git a/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap b/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap
new file mode 100644
index 00000000000000..c023452f3bf5e6
--- /dev/null
+++ b/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap
@@ -0,0 +1,25 @@
+---
+source: src/rules/flake8_raise/mod.rs
+expression: diagnostics
+---
+- kind:
+    UnnecessaryParenOnRaiseException: ~
+  location:
+    row: 4
+    column: 10
+  end_location:
+    row: 4
+    column: 22
+  fix: ~
+  parent: ~
+- kind:
+    UnnecessaryParenOnRaiseException: ~
+  location:
+    row: 11
+    column: 6
+  end_location:
+    row: 11
+    column: 17
+  fix: ~
+  parent: ~
+
diff --git a/src/rules/mod.rs b/src/rules/mod.rs
index 30f9c28d255a65..38ae33a825c3ce 100644
--- a/src/rules/mod.rs
+++ b/src/rules/mod.rs
@@ -21,6 +21,7 @@ pub mod flake8_pie;
 pub mod flake8_print;
 pub mod flake8_pytest_style;
 pub mod flake8_quotes;
+pub mod flake8_raise;
 pub mod flake8_return;
 pub mod flake8_simplify;
 pub mod flake8_tidy_imports;