Repository: astral-sh/ruff
File: crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs (Rust)

Code before:
        let [elt] = elts.as_slice() else {
            return vec![];
        };

        if matches!(elt, Expr::Starred(_)) {
            return vec![];
        }

        edits.push(Edit::range_replacement(
            unparse_expr_in_sequence(elt, checker),
            value.range(),
        ));
    }
    edits
}

fn unparse_expr_in_sequence(expr: &Expr, checker: &Checker) -> String {
    let content = checker.generator().expr(expr);
    // The generator may unparse tuples without outer parentheses at level 0.
    // For example, `(1, 2)` becomes `1, 2` and `((1,),)` becomes `(1,),`.
    // When these are used as elements in a list, it causes syntax errors.
    // We detect this by checking if the content ends with a trailing comma
    // that is not inside parentheses.
    if let Expr::Tuple(tuple) = expr {
        if !tuple.is_empty() && !is_parenthesized(&content) {
            return format!("({content})");
        }
    }
    content
}

/// Check if the content is fully parenthesized (starts with '(' and the matching ')' is at the end).
fn is_parenthesized(content: &str) -> bool {
    if !content.starts_with('(') {
        return false;
    }
    let mut depth = 0;
    for (i, c) in content.chars().enumerate() {
        match c {
            '(' => depth += 1,
            ')' => {
                depth -= 1;
                if depth == 0 {
                    // If we close the first paren before the end, it's not fully parenthesized
                    return i == content.len() - 1;
                }
            }
            _ => {}
        }
    }
    false

Review comment (suggestion):
maybe:

```diff
diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/par
ametrize.rs
index d1a5240a15..537687d571 100644
--- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs
+++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs
@@ -753,41 +753,19 @@ fn unpack_single_element_items(checker: &Checker, expr: &Expr) -> Vec<Edit> {
 
 fn unparse_expr_in_sequence(expr: &Expr, checker: &Checker) -> String {
     let content = checker.generator().expr(expr);
-    // The generator may unparse tuples without outer parentheses at level 0.
-    // For example, `(1, 2)` becomes `1, 2` and `((1,),)` becomes `(1,),`.
-    // When these are used as elements in a list, it causes syntax errors.
-    // We detect this by checking if the content ends with a trailing comma
-    // that is not inside parentheses.
+
     if let Expr::Tuple(tuple) = expr {
-        if !tuple.is_empty() && !is_parenthesized(&content) {
+        if !tuple.is_empty() && parenthesized_range(
+            expr.into(),
+            checker.semantic().current_statement().into(),
+            checker.tokens()
+        ).is_none() {
             return format!("({content})");
         }
     }
     content
 }
 
-/// Check if the content is fully parenthesized (starts with '(' and the matching ')' is at the end).
-fn is_parenthesized(content: &str) -> bool {
-    if !content.starts_with('(') {
-        return false;
-    }
-    let mut depth = 0;
-    for (i, c) in content.chars().enumerate() {
-        match c {
-            '(' => depth += 1,
-            ')' => {
-                depth -= 1;
-                if depth == 0 {
-                    // If we close the first paren before the end, it's not fully parenthesized
-                    return i == content.len() - 1;
-                }
-            }
-            _ => {}
-        }
-    }
-    false
-}
-
 fn handle_value_rows(
     checker: &Checker,
     elts: &[Expr],
```

Code after:
        let [elt] = elts.as_slice() else {
            return vec![];
        };

        if matches!(elt, Expr::Starred(_)) {
            return vec![];
        }

        edits.push(Edit::range_replacement(
            unparse_expr_in_sequence(elt, checker),
            value.range(),
        ));
    }
    edits
}

fn unparse_expr_in_sequence(expr: &Expr, checker: &Checker) -> String {
    let content = checker.locator().slice(expr);
    if let Expr::Tuple(tuple) = expr {
        if !tuple.is_empty() && !tuple.parenthesized {
            return format!("({content})");
        }
    }
    content.to_string()
}

fn handle_value_rows(
    checker: &Checker,
    elts: &[Expr],
    values_type: types::ParametrizeValuesType,
    values_row_type: types::ParametrizeValuesRowType,
) {
    for elt in elts {
        match elt {
            Expr::Tuple(ast::ExprTuple { elts, .. }) => {
                if values_row_type != types::ParametrizeValuesRowType::Tuple {
                    let mut diagnostic = checker.report_diagnostic(
                        PytestParametrizeValuesWrongType {
                            values: values_type,
                            row: values_row_type,
                        },
                        elt.range(),
                    );
                    diagnostic.set_fix({
                        // Determine whether a trailing comma is present due to the _requirement_
                        // that a single-element tuple must have a trailing comma, e.g., `(1,)`.
                        //
                        // If the trailing comma is on its own line, we intentionally ignore it,
                        // since the expression is already split over multiple lines, as in:
                        // ```python
                        // @pytest.mark.parametrize(