Skip to main content

rustpython_ruff_python_ast/
parenthesize.rs

1use ruff_python_trivia::{BackwardsTokenizer, CommentRanges, SimpleTokenKind, SimpleTokenizer};
2use ruff_text_size::{Ranged, TextLen, TextRange};
3
4use crate::AnyNodeRef;
5use crate::ExprRef;
6
7/// Returns an iterator over the ranges of the optional parentheses surrounding an expression.
8///
9/// E.g. for `((f()))` with `f()` as expression, the iterator returns the ranges (1, 6) and (0, 7).
10///
11/// Note that without a parent the range can be inaccurate, e.g. `f(a)` we falsely return a set of
12/// parentheses around `a` even if the parentheses actually belong to `f`. That is why you should
13/// generally prefer [`parenthesized_range`].
14///
15/// Prefer [`crate::token::parentheses_iterator`] if you have access to [`crate::token::Tokens`].
16pub fn parentheses_iterator<'a>(
17    expr: ExprRef<'a>,
18    parent: Option<AnyNodeRef>,
19    comment_ranges: &'a CommentRanges,
20    source: &'a str,
21) -> impl Iterator<Item = TextRange> + 'a {
22    let right_tokenizer = if let Some(parent) = parent {
23        // If the parent is a node that brings its own parentheses, exclude the closing parenthesis
24        // from our search range. Otherwise, we risk matching on calls, like `func(x)`, for which
25        // the open and close parentheses are part of the `Arguments` node.
26        //
27        // There are a few other nodes that may have their own parentheses, but are fine to exclude:
28        // - `Parameters`: The parameters to a function definition. Any expressions would represent
29        //   default arguments, and so must be preceded by _at least_ the parameter name. As such,
30        //   we won't mistake any parentheses for the opening and closing parentheses on the
31        //   `Parameters` node itself.
32        // - `Tuple`: The elements of a tuple. The only risk is a single-element tuple (e.g., `(x,)`),
33        //    which must have a trailing comma anyway.
34        let exclusive_parent_end = if parent.is_arguments() {
35            parent.end() - ")".text_len()
36        } else {
37            parent.end()
38        };
39        SimpleTokenizer::new(source, TextRange::new(expr.end(), exclusive_parent_end))
40    } else {
41        SimpleTokenizer::starts_at(expr.end(), source)
42    };
43
44    let right_tokenizer = right_tokenizer
45        .skip_trivia()
46        .take_while(|token| token.kind == SimpleTokenKind::RParen);
47
48    let left_tokenizer = BackwardsTokenizer::up_to(expr.start(), source, comment_ranges)
49        .skip_trivia()
50        .take_while(|token| token.kind == SimpleTokenKind::LParen);
51
52    // Zip closing parenthesis with opening parenthesis. The order is intentional, as testing for
53    // closing parentheses is cheaper, and `zip` will avoid progressing the `left_tokenizer` if
54    // the `right_tokenizer` is exhausted.
55    right_tokenizer
56        .zip(left_tokenizer)
57        .map(|(right, left)| TextRange::new(left.start(), right.end()))
58}
59
60/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is
61/// parenthesized; or `None`, if the expression is not parenthesized.
62///
63/// Prefer [`crate::token::parenthesized_range`] if you have access to [`crate::token::Tokens`].
64pub fn parenthesized_range(
65    expr: ExprRef,
66    parent: AnyNodeRef,
67    comment_ranges: &CommentRanges,
68    source: &str,
69) -> Option<TextRange> {
70    parentheses_iterator(expr, Some(parent), comment_ranges, source).last()
71}