Skip to content

Commit f4e4229

Browse files
Add token based parenthesized_ranges implementation (#21738)
Co-authored-by: Micha Reiser <[email protected]>
1 parent e6ddeed commit f4e4229

File tree

6 files changed

+339
-80
lines changed

6 files changed

+339
-80
lines changed

crates/ruff_python_ast/src/parenthesize.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use crate::ExprRef;
1111
/// Note that without a parent the range can be inaccurate, e.g. `f(a)` we falsely return a set of
1212
/// parentheses around `a` even if the parentheses actually belong to `f`. That is why you should
1313
/// generally prefer [`parenthesized_range`].
14+
///
15+
/// Prefer [`crate::token::parentheses_iterator`] if you have access to [`crate::token::Tokens`].
1416
pub fn parentheses_iterator<'a>(
1517
expr: ExprRef<'a>,
1618
parent: Option<AnyNodeRef>,
@@ -57,6 +59,8 @@ pub fn parentheses_iterator<'a>(
5759

5860
/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is
5961
/// parenthesized; or `None`, if the expression is not parenthesized.
62+
///
63+
/// Prefer [`crate::token::parenthesized_range`] if you have access to [`crate::token::Tokens`].
6064
pub fn parenthesized_range(
6165
expr: ExprRef,
6266
parent: AnyNodeRef,

crates/ruff_python_ast/src/token.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ use crate::str_prefix::{
1616
use crate::{AnyStringFlags, BoolOp, Operator, StringFlags, UnaryOp};
1717
use ruff_text_size::{Ranged, TextRange};
1818

19+
mod parentheses;
1920
mod tokens;
2021

22+
pub use parentheses::{parentheses_iterator, parenthesized_range};
2123
pub use tokens::{TokenAt, TokenIterWithContext, Tokens};
2224

2325
#[derive(Clone, Copy, PartialEq, Eq)]
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use ruff_text_size::{Ranged, TextLen, TextRange};
2+
3+
use super::{TokenKind, Tokens};
4+
use crate::{AnyNodeRef, ExprRef};
5+
6+
/// Returns an iterator over the ranges of the optional parentheses surrounding an expression.
7+
///
8+
/// E.g. for `((f()))` with `f()` as expression, the iterator returns the ranges (1, 6) and (0, 7).
9+
///
10+
/// Note that without a parent the range can be inaccurate, e.g. `f(a)` we falsely return a set of
11+
/// parentheses around `a` even if the parentheses actually belong to `f`. That is why you should
12+
/// generally prefer [`parenthesized_range`].
13+
pub fn parentheses_iterator<'a>(
14+
expr: ExprRef<'a>,
15+
parent: Option<AnyNodeRef>,
16+
tokens: &'a Tokens,
17+
) -> impl Iterator<Item = TextRange> + 'a {
18+
let after_tokens = if let Some(parent) = parent {
19+
// If the parent is a node that brings its own parentheses, exclude the closing parenthesis
20+
// from our search range. Otherwise, we risk matching on calls, like `func(x)`, for which
21+
// the open and close parentheses are part of the `Arguments` node.
22+
let exclusive_parent_end = if parent.is_arguments() {
23+
parent.end() - ")".text_len()
24+
} else {
25+
parent.end()
26+
};
27+
28+
tokens.in_range(TextRange::new(expr.end(), exclusive_parent_end))
29+
} else {
30+
tokens.after(expr.end())
31+
};
32+
33+
let right_parens = after_tokens
34+
.iter()
35+
.filter(|token| !token.kind().is_trivia())
36+
.take_while(move |token| token.kind() == TokenKind::Rpar);
37+
38+
let left_parens = tokens
39+
.before(expr.start())
40+
.iter()
41+
.rev()
42+
.filter(|token| !token.kind().is_trivia())
43+
.take_while(|token| token.kind() == TokenKind::Lpar);
44+
45+
right_parens
46+
.zip(left_parens)
47+
.map(|(right, left)| TextRange::new(left.start(), right.end()))
48+
}
49+
50+
/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is
51+
/// parenthesized; or `None`, if the expression is not parenthesized.
52+
pub fn parenthesized_range(
53+
expr: ExprRef,
54+
parent: AnyNodeRef,
55+
tokens: &Tokens,
56+
) -> Option<TextRange> {
57+
parentheses_iterator(expr, Some(parent), tokens).last()
58+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
//! Tests for [`ruff_python_ast::tokens::parentheses_iterator`] and
2+
//! [`ruff_python_ast::tokens::parenthesized_range`].
3+
4+
use ruff_python_ast::{
5+
self as ast, Expr,
6+
token::{parentheses_iterator, parenthesized_range},
7+
};
8+
use ruff_python_parser::parse_module;
9+
10+
#[test]
11+
fn test_no_parentheses() {
12+
let source = "x = 2 + 2";
13+
let parsed = parse_module(source).expect("should parse valid python");
14+
let tokens = parsed.tokens();
15+
let module = parsed.syntax();
16+
17+
let stmt = module.body.first().expect("module should have a statement");
18+
let ast::Stmt::Assign(assign) = stmt else {
19+
panic!("expected `Assign` statement, got {stmt:?}");
20+
};
21+
22+
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
23+
assert_eq!(result, None);
24+
}
25+
26+
#[test]
27+
fn test_single_parentheses() {
28+
let source = "x = (2 + 2)";
29+
let parsed = parse_module(source).expect("should parse valid python");
30+
let tokens = parsed.tokens();
31+
let module = parsed.syntax();
32+
33+
let stmt = module.body.first().expect("module should have a statement");
34+
let ast::Stmt::Assign(assign) = stmt else {
35+
panic!("expected `Assign` statement, got {stmt:?}");
36+
};
37+
38+
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
39+
let range = result.expect("should find parentheses");
40+
assert_eq!(&source[range], "(2 + 2)");
41+
}
42+
43+
#[test]
44+
fn test_double_parentheses() {
45+
let source = "x = ((2 + 2))";
46+
let parsed = parse_module(source).expect("should parse valid python");
47+
let tokens = parsed.tokens();
48+
let module = parsed.syntax();
49+
50+
let stmt = module.body.first().expect("module should have a statement");
51+
let ast::Stmt::Assign(assign) = stmt else {
52+
panic!("expected `Assign` statement, got {stmt:?}");
53+
};
54+
55+
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
56+
let range = result.expect("should find parentheses");
57+
assert_eq!(&source[range], "((2 + 2))");
58+
}
59+
60+
#[test]
61+
fn test_parentheses_with_whitespace() {
62+
let source = "x = ( 2 + 2 )";
63+
let parsed = parse_module(source).expect("should parse valid python");
64+
let tokens = parsed.tokens();
65+
let module = parsed.syntax();
66+
67+
let stmt = module.body.first().expect("module should have a statement");
68+
let ast::Stmt::Assign(assign) = stmt else {
69+
panic!("expected `Assign` statement, got {stmt:?}");
70+
};
71+
72+
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
73+
let range = result.expect("should find parentheses");
74+
assert_eq!(&source[range], "( 2 + 2 )");
75+
}
76+
77+
#[test]
78+
fn test_parentheses_with_comments() {
79+
let source = "x = ( # comment\n 2 + 2\n)";
80+
let parsed = parse_module(source).expect("should parse valid python");
81+
let tokens = parsed.tokens();
82+
let module = parsed.syntax();
83+
84+
let stmt = module.body.first().expect("module should have a statement");
85+
let ast::Stmt::Assign(assign) = stmt else {
86+
panic!("expected `Assign` statement, got {stmt:?}");
87+
};
88+
89+
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
90+
let range = result.expect("should find parentheses");
91+
assert_eq!(&source[range], "( # comment\n 2 + 2\n)");
92+
}
93+
94+
#[test]
95+
fn test_parenthesized_range_multiple() {
96+
let source = "x = (((2 + 2)))";
97+
let parsed = parse_module(source).expect("should parse valid python");
98+
let tokens = parsed.tokens();
99+
let module = parsed.syntax();
100+
101+
let stmt = module.body.first().expect("module should have a statement");
102+
let ast::Stmt::Assign(assign) = stmt else {
103+
panic!("expected `Assign` statement, got {stmt:?}");
104+
};
105+
106+
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
107+
let range = result.expect("should find parentheses");
108+
assert_eq!(&source[range], "(((2 + 2)))");
109+
}
110+
111+
#[test]
112+
fn test_parentheses_iterator_multiple() {
113+
let source = "x = (((2 + 2)))";
114+
let parsed = parse_module(source).expect("should parse valid python");
115+
let tokens = parsed.tokens();
116+
let module = parsed.syntax();
117+
118+
let stmt = module.body.first().expect("module should have a statement");
119+
let ast::Stmt::Assign(assign) = stmt else {
120+
panic!("expected `Assign` statement, got {stmt:?}");
121+
};
122+
123+
let ranges: Vec<_> =
124+
parentheses_iterator(assign.value.as_ref().into(), Some(stmt.into()), tokens).collect();
125+
assert_eq!(ranges.len(), 3);
126+
assert_eq!(&source[ranges[0]], "(2 + 2)");
127+
assert_eq!(&source[ranges[1]], "((2 + 2))");
128+
assert_eq!(&source[ranges[2]], "(((2 + 2)))");
129+
}
130+
131+
#[test]
132+
fn test_call_arguments_not_counted() {
133+
let source = "f(x)";
134+
let parsed = parse_module(source).expect("should parse valid python");
135+
let tokens = parsed.tokens();
136+
let module = parsed.syntax();
137+
138+
let stmt = module.body.first().expect("module should have a statement");
139+
let ast::Stmt::Expr(expr_stmt) = stmt else {
140+
panic!("expected `Expr` statement, got {stmt:?}");
141+
};
142+
143+
let Expr::Call(call) = expr_stmt.value.as_ref() else {
144+
panic!("expected Call expression, got {:?}", expr_stmt.value);
145+
};
146+
147+
let arg = call
148+
.arguments
149+
.args
150+
.first()
151+
.expect("call should have an argument");
152+
let result = parenthesized_range(arg.into(), (&call.arguments).into(), tokens);
153+
// The parentheses belong to the call, not the argument
154+
assert_eq!(result, None);
155+
}
156+
157+
#[test]
158+
fn test_call_with_parenthesized_argument() {
159+
let source = "f((x))";
160+
let parsed = parse_module(source).expect("should parse valid python");
161+
let tokens = parsed.tokens();
162+
let module = parsed.syntax();
163+
164+
let stmt = module.body.first().expect("module should have a statement");
165+
let ast::Stmt::Expr(expr_stmt) = stmt else {
166+
panic!("expected Expr statement, got {stmt:?}");
167+
};
168+
169+
let Expr::Call(call) = expr_stmt.value.as_ref() else {
170+
panic!("expected `Call` expression, got {:?}", expr_stmt.value);
171+
};
172+
173+
let arg = call
174+
.arguments
175+
.args
176+
.first()
177+
.expect("call should have an argument");
178+
let result = parenthesized_range(arg.into(), (&call.arguments).into(), tokens);
179+
180+
let range = result.expect("should find parentheses around argument");
181+
assert_eq!(&source[range], "(x)");
182+
}
183+
184+
#[test]
185+
fn test_multiline_with_parentheses() {
186+
let source = "x = (\n 2 + 2 + 2\n)";
187+
let parsed = parse_module(source).expect("should parse valid python");
188+
let tokens = parsed.tokens();
189+
let module = parsed.syntax();
190+
191+
let stmt = module.body.first().expect("module should have a statement");
192+
let ast::Stmt::Assign(assign) = stmt else {
193+
panic!("expected `Assign` statement, got {stmt:?}");
194+
};
195+
196+
let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens);
197+
let range = result.expect("should find parentheses");
198+
assert_eq!(&source[range], "(\n 2 + 2 + 2\n)");
199+
}

0 commit comments

Comments
 (0)