From 602b7ea0e2d83e6f436c53bdb17896a1c886b300 Mon Sep 17 00:00:00 2001 From: Denys Zhak Date: Mon, 1 Dec 2025 20:07:31 +0000 Subject: [PATCH 1/7] feat: optimize parenthesized_range --- .../tests/parenthesize_optimized.rs | 298 ++++++++++++++++++ crates/ruff_python_parser/src/lib.rs | 1 + crates/ruff_python_parser/src/parenthesize.rs | 270 ++++++++++++++++ crates/ty/docs/rules.md | 148 ++++----- .../src/types/diagnostic.rs | 8 +- 5 files changed, 645 insertions(+), 80 deletions(-) create mode 100644 crates/ruff_python_ast_integration_tests/tests/parenthesize_optimized.rs create mode 100644 crates/ruff_python_parser/src/parenthesize.rs diff --git a/crates/ruff_python_ast_integration_tests/tests/parenthesize_optimized.rs b/crates/ruff_python_ast_integration_tests/tests/parenthesize_optimized.rs new file mode 100644 index 0000000000000..62f34e4bd3cc9 --- /dev/null +++ b/crates/ruff_python_ast_integration_tests/tests/parenthesize_optimized.rs @@ -0,0 +1,298 @@ +use ruff_python_ast::parenthesize as original_parenthesize; +use ruff_python_parser::parenthesize as optimized_parenthesize; +use ruff_python_parser::parse_expression; +use ruff_python_trivia::CommentRanges; + +#[test] +fn test_optimized_vs_original_parenthesized_name() { + let source_code = r"(x) + 1"; + let parsed = parse_expression(source_code).unwrap(); + + let bin_op = parsed.expr().as_bin_op_expr().unwrap(); + let name = bin_op.left.as_ref(); + + let original = original_parenthesize::parenthesized_range( + name.into(), + bin_op.into(), + &CommentRanges::default(), + source_code, + ); + + let optimized = + optimized_parenthesize::parenthesized_range(name.into(), bin_op.into(), parsed.tokens()); + + assert_eq!(original, optimized); + let range = optimized.expect("should find parentheses"); + assert_eq!(&source_code[range], "(x)"); +} + +#[test] +fn test_optimized_vs_original_non_parenthesized_name() { + let source_code = r"x + 1"; + let parsed = parse_expression(source_code).unwrap(); + + let bin_op = parsed.expr().as_bin_op_expr().unwrap(); + let name = bin_op.left.as_ref(); + + let original = original_parenthesize::parenthesized_range( + name.into(), + bin_op.into(), + &CommentRanges::default(), + source_code, + ); + + let optimized = + optimized_parenthesize::parenthesized_range(name.into(), bin_op.into(), parsed.tokens()); + + assert_eq!(original, optimized); + assert_eq!(optimized, None); +} + +#[test] +fn test_optimized_vs_original_parenthesized_argument() { + let source_code = r"f((a))"; + let parsed = parse_expression(source_code).unwrap(); + + let call = parsed.expr().as_call_expr().unwrap(); + let arguments = &call.arguments; + let argument = arguments.args.first().unwrap(); + + let original = original_parenthesize::parenthesized_range( + argument.into(), + arguments.into(), + &CommentRanges::default(), + source_code, + ); + + let optimized = optimized_parenthesize::parenthesized_range( + argument.into(), + arguments.into(), + parsed.tokens(), + ); + + assert_eq!(original, optimized); + let range = optimized.expect("should find parentheses"); + assert_eq!(&source_code[range], "(a)"); +} + +#[test] +fn test_optimized_vs_original_non_parenthesized_argument() { + let source_code = r"f(a)"; + let parsed = parse_expression(source_code).unwrap(); + + let call = parsed.expr().as_call_expr().unwrap(); + let arguments = &call.arguments; + let argument = arguments.args.first().unwrap(); + + let original = original_parenthesize::parenthesized_range( + argument.into(), + arguments.into(), + &CommentRanges::default(), + source_code, + ); + + let optimized = optimized_parenthesize::parenthesized_range( + argument.into(), + arguments.into(), + parsed.tokens(), + ); + + assert_eq!(original, optimized); + assert_eq!(optimized, None); +} + +#[test] +fn test_optimized_vs_original_twice_parenthesized() { + let source_code = r"((x)) + 1"; + let parsed = parse_expression(source_code).unwrap(); + + let bin_op = parsed.expr().as_bin_op_expr().unwrap(); + let name = bin_op.left.as_ref(); + + let original = original_parenthesize::parenthesized_range( + name.into(), + bin_op.into(), + &CommentRanges::default(), + source_code, + ); + + let optimized = + optimized_parenthesize::parenthesized_range(name.into(), bin_op.into(), parsed.tokens()); + + assert_eq!(original, optimized); + let range = optimized.expect("should find parentheses"); + assert_eq!(&source_code[range], "((x))"); +} + +#[test] +fn test_optimized_vs_original_with_whitespace() { + let source_code = r"( x ) + 1"; + let parsed = parse_expression(source_code).unwrap(); + + let bin_op = parsed.expr().as_bin_op_expr().unwrap(); + let name = bin_op.left.as_ref(); + + let original = original_parenthesize::parenthesized_range( + name.into(), + bin_op.into(), + &CommentRanges::default(), + source_code, + ); + + let optimized = + optimized_parenthesize::parenthesized_range(name.into(), bin_op.into(), parsed.tokens()); + + assert_eq!(original, optimized); + let range = optimized.expect("should find parentheses"); + assert_eq!(&source_code[range], "( x )"); +} + +#[test] +fn test_optimized_vs_original_with_comments() { + let source_code = r"( # comment + x +) + 1"; + let parsed = parse_expression(source_code).unwrap(); + + let bin_op = parsed.expr().as_bin_op_expr().unwrap(); + let name = bin_op.left.as_ref(); + + let comment_ranges = CommentRanges::from(parsed.tokens()); + + let original = original_parenthesize::parenthesized_range( + name.into(), + bin_op.into(), + &comment_ranges, + source_code, + ); + + let optimized = + optimized_parenthesize::parenthesized_range(name.into(), bin_op.into(), parsed.tokens()); + + assert_eq!(original, optimized); + let range = optimized.expect("should find parentheses"); + assert_eq!(&source_code[range], "( # comment\n x\n)"); +} + +#[test] +fn test_optimized_vs_original_multiple_layers() { + let source_code = r"(((x))) + 1"; + let parsed = parse_expression(source_code).unwrap(); + + let bin_op = parsed.expr().as_bin_op_expr().unwrap(); + let name = bin_op.left.as_ref(); + + let original = original_parenthesize::parenthesized_range( + name.into(), + bin_op.into(), + &CommentRanges::default(), + source_code, + ); + + let optimized = + optimized_parenthesize::parenthesized_range(name.into(), bin_op.into(), parsed.tokens()); + + assert_eq!(original, optimized); + let range = optimized.expect("should find parentheses"); + assert_eq!(&source_code[range], "(((x)))"); +} + +#[test] +fn test_optimized_vs_original_iterator_all_layers() { + let source_code = r"(((x))) + 1"; + let parsed = parse_expression(source_code).unwrap(); + + let bin_op = parsed.expr().as_bin_op_expr().unwrap(); + let name = bin_op.left.as_ref(); + + let original_layers: Vec<_> = original_parenthesize::parentheses_iterator( + name.into(), + Some(bin_op.into()), + &CommentRanges::default(), + source_code, + ) + .collect(); + + let optimized_layers: Vec<_> = optimized_parenthesize::parentheses_iterator( + name.into(), + Some(bin_op.into()), + parsed.tokens(), + ) + .collect(); + + assert_eq!(original_layers, optimized_layers); + assert_eq!(optimized_layers.len(), 3); + assert_eq!(&source_code[optimized_layers[0]], "(x)"); + assert_eq!(&source_code[optimized_layers[1]], "((x))"); + assert_eq!(&source_code[optimized_layers[2]], "(((x)))"); +} + +#[test] +fn test_optimized_vs_original_complex_expression() { + let source_code = r"((a + b) * (c - d))"; + let parsed = parse_expression(source_code).unwrap(); + + let outer_paren = parsed.expr().as_bin_op_expr().unwrap(); + + let original = original_parenthesize::parenthesized_range( + outer_paren.into(), + outer_paren.into(), + &CommentRanges::default(), + source_code, + ); + + let optimized = optimized_parenthesize::parenthesized_range( + outer_paren.into(), + outer_paren.into(), + parsed.tokens(), + ); + + assert_eq!(original, optimized); +} + +#[test] +fn test_optimized_vs_original_tuple_member() { + let source_code = r"(a, (b))"; + let parsed = parse_expression(source_code).unwrap(); + + let tuple = parsed.expr().as_tuple_expr().unwrap(); + let member = tuple.elts.last().unwrap(); + + let original = original_parenthesize::parenthesized_range( + member.into(), + tuple.into(), + &CommentRanges::default(), + source_code, + ); + + let optimized = + optimized_parenthesize::parenthesized_range(member.into(), tuple.into(), parsed.tokens()); + + assert_eq!(original, optimized); + let range = optimized.expect("should find parentheses"); + assert_eq!(&source_code[range], "(b)"); +} + +#[test] +fn test_optimized_vs_original_nested_calls() { + let source_code = r"f(g((h(x))))"; + let parsed = parse_expression(source_code).unwrap(); + + let outer_call = parsed.expr().as_call_expr().unwrap(); + let inner_arg = outer_call.arguments.args.first().unwrap(); + + let original = original_parenthesize::parenthesized_range( + inner_arg.into(), + (&outer_call.arguments).into(), + &CommentRanges::default(), + source_code, + ); + + let optimized = optimized_parenthesize::parenthesized_range( + inner_arg.into(), + (&outer_call.arguments).into(), + parsed.tokens(), + ); + + assert_eq!(original, optimized); +} diff --git a/crates/ruff_python_parser/src/lib.rs b/crates/ruff_python_parser/src/lib.rs index ce409200ae29f..8a1f83c189c1e 100644 --- a/crates/ruff_python_parser/src/lib.rs +++ b/crates/ruff_python_parser/src/lib.rs @@ -83,6 +83,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; mod error; pub mod lexer; +pub mod parenthesize; mod parser; pub mod semantic_errors; mod string; diff --git a/crates/ruff_python_parser/src/parenthesize.rs b/crates/ruff_python_parser/src/parenthesize.rs new file mode 100644 index 0000000000000..dc4d9df16e2e4 --- /dev/null +++ b/crates/ruff_python_parser/src/parenthesize.rs @@ -0,0 +1,270 @@ +use ruff_python_ast::{AnyNodeRef, ExprRef}; +use ruff_text_size::{Ranged, TextLen, TextRange}; + +use crate::{TokenKind, Tokens}; + +/// Tokens that should be treated as trivia when scanning around parentheses. +/// Mirrors the behavior of `SimpleTokenKind::is_trivia()` as closely as possible +/// at the `TokenKind` level. +const fn is_trivia(kind: TokenKind) -> bool { + matches!( + kind, + TokenKind::Comment + | TokenKind::Newline + | TokenKind::NonLogicalNewline + | TokenKind::Indent + | TokenKind::Dedent + ) +} + +/// Returns an iterator over the ranges of the optional parentheses surrounding an expression. +/// +/// E.g. for `((f()))` with `f()` as expression, the iterator returns the ranges (1, 6) and (0, 7). +/// +/// Note that without a parent the range can be inaccurate, e.g. `f(a)` we falsely return a set of +/// parentheses around `a` even if the parentheses actually belong to `f`. That is why you should +/// generally prefer [`parenthesized_range`]. +pub fn parentheses_iterator<'a>( + expr: ExprRef<'a>, + parent: Option, + tokens: &'a Tokens, +) -> impl Iterator + 'a { + let exclusive_parent_end = if let Some(parent) = parent { + // If the parent is a node that brings its own parentheses, exclude the closing parenthesis + // from our search range. Otherwise, we risk matching on calls, like `func(x)`, for which + // the open and close parentheses are part of the `Arguments` node. + if parent.is_arguments() { + parent.end() - ")".text_len() + } else { + parent.end() + } + } else { + tokens.last().map_or(expr.end(), |t| t.end()) + }; + + let right_parens = tokens + .after(expr.end()) + .iter() + .take_while(move |token| token.start() < exclusive_parent_end) + .filter(|token| !is_trivia(token.kind())) + .take_while(|token| token.kind() == TokenKind::Rpar); + + let left_parens = tokens + .before(expr.start()) + .iter() + .rev() + .filter(|token| !is_trivia(token.kind())) + .take_while(|token| token.kind() == TokenKind::Lpar); + + right_parens + .zip(left_parens) + .map(|(right, left)| TextRange::new(left.start(), right.end())) +} + +/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is +/// parenthesized; or `None`, if the expression is not parenthesized. +pub fn parenthesized_range( + expr: ExprRef, + parent: AnyNodeRef, + tokens: &Tokens, +) -> Option { + parentheses_iterator(expr, Some(parent), tokens).last() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parse_module; + use ruff_python_ast::{self as ast, Expr}; + + #[test] + fn test_no_parentheses() { + let source = "x = 2 + 2"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + assert_eq!(result, None); + } + + #[test] + fn test_single_parentheses() { + let source = "x = (2 + 2)"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + let range = result.expect("should find parentheses"); + assert_eq!(&source[range], "(2 + 2)"); + } + + #[test] + fn test_double_parentheses() { + let source = "x = ((2 + 2))"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + let range = result.expect("should find parentheses"); + assert_eq!(&source[range], "((2 + 2))"); + } + + #[test] + fn test_parentheses_with_whitespace() { + let source = "x = ( 2 + 2 )"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + let range = result.expect("should find parentheses"); + assert_eq!(&source[range], "( 2 + 2 )"); + } + + #[test] + fn test_parentheses_with_comments() { + let source = "x = ( # comment\n 2 + 2\n)"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + let range = result.expect("should find parentheses"); + assert_eq!(&source[range], "( # comment\n 2 + 2\n)"); + } + + #[test] + fn test_parenthesized_range_multiple() { + let source = "x = (((2 + 2)))"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + let range = result.expect("should find parentheses"); + assert_eq!(&source[range], "(((2 + 2)))"); + } + + #[test] + fn test_parentheses_iterator_multiple() { + let source = "x = (((2 + 2)))"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let ranges: Vec<_> = + parentheses_iterator(assign.value.as_ref().into(), Some(stmt.into()), tokens).collect(); + assert_eq!(ranges.len(), 3); + assert_eq!(&source[ranges[0]], "(2 + 2)"); + assert_eq!(&source[ranges[1]], "((2 + 2))"); + assert_eq!(&source[ranges[2]], "(((2 + 2)))"); + } + + #[test] + fn test_call_arguments_not_counted() { + let source = "f(x)"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Expr(expr_stmt) = stmt else { + panic!("expected `Expr` statement, got {stmt:?}"); + }; + + let Expr::Call(call) = expr_stmt.value.as_ref() else { + panic!("expected Call expression, got {:?}", expr_stmt.value); + }; + + let arg = call + .arguments + .args + .first() + .expect("call should have an argument"); + let result = parenthesized_range(arg.into(), (&call.arguments).into(), tokens); + // The parentheses belong to the call, not the argument + assert_eq!(result, None); + } + + #[test] + fn test_call_with_parenthesized_argument() { + let source = "f((x))"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Expr(expr_stmt) = stmt else { + panic!("expected Expr statement, got {stmt:?}"); + }; + + let Expr::Call(call) = expr_stmt.value.as_ref() else { + panic!("expected `Call` expression, got {:?}", expr_stmt.value); + }; + + let arg = call + .arguments + .args + .first() + .expect("call should have an argument"); + let result = parenthesized_range(arg.into(), (&call.arguments).into(), tokens); + + let range = result.expect("should find parentheses around argument"); + assert_eq!(&source[range], "(x)"); + } + + #[test] + fn test_multiline_with_parentheses() { + let source = "x = (\n 2 + 2 + 2\n)"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + let range = result.expect("should find parentheses"); + assert_eq!(&source[range], "(\n 2 + 2 + 2\n)"); + } +} diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 78f757a6bb291..0fddeff65f19d 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -39,7 +39,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -95,7 +95,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -126,7 +126,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -158,7 +158,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -190,7 +190,7 @@ class B(A): ... Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -218,7 +218,7 @@ type B = A Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -245,7 +245,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -357,7 +357,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -387,7 +387,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -413,7 +413,7 @@ t[3] # IndexError: tuple index out of range Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -502,7 +502,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -557,7 +557,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -591,7 +591,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -627,7 +627,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -678,7 +678,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -707,7 +707,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -751,7 +751,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -793,7 +793,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -826,7 +826,7 @@ class C[U](Generic[T]): ... Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -865,7 +865,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -900,7 +900,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -934,7 +934,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1041,7 +1041,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1073,7 +1073,7 @@ TypeError: can only inherit from a NamedTuple type and Generic Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -1103,7 +1103,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1153,7 +1153,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1179,7 +1179,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1210,7 +1210,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1244,7 +1244,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1293,7 +1293,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1318,7 +1318,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1376,7 +1376,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1403,7 +1403,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1450,7 +1450,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1480,7 +1480,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1510,7 +1510,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1544,7 +1544,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1578,7 +1578,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1613,7 +1613,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1638,7 +1638,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1671,7 +1671,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1700,7 +1700,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1724,7 +1724,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1750,7 +1750,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1783,7 +1783,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1810,7 +1810,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1868,7 +1868,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1898,7 +1898,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1927,7 +1927,7 @@ class B(A): ... # Error raised here Default level: error · Preview (since 0.0.1-alpha.30) · Related issues · -View source +View source @@ -1961,7 +1961,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1988,7 +1988,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2016,7 +2016,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2062,7 +2062,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2089,7 +2089,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2117,7 +2117,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2142,7 +2142,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2167,7 +2167,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2204,7 +2204,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2232,7 +2232,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2257,7 +2257,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2298,7 +2298,7 @@ class SubProto(BaseProto, Protocol): Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -2386,7 +2386,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2414,7 +2414,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2446,7 +2446,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2478,7 +2478,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2505,7 +2505,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2529,7 +2529,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2587,7 +2587,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2626,7 +2626,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2689,7 +2689,7 @@ def foo(x: int | str) -> int | str: Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -2713,7 +2713,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index f4f62a0f0b158..87392f3bcecb5 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -35,13 +35,11 @@ use itertools::Itertools; use ruff_db::{ diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}, parsed::parsed_module, - source::source_text, }; use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::name::Name; -use ruff_python_ast::parenthesize::parentheses_iterator; use ruff_python_ast::{self as ast, AnyNodeRef, StringFlags}; -use ruff_python_trivia::CommentRanges; +use ruff_python_parser::parenthesize::parentheses_iterator; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; use std::fmt::{self, Formatter}; @@ -2399,9 +2397,7 @@ pub(super) fn report_invalid_assignment<'db>( // ) # ty: ignore <- or here // ``` - let comment_ranges = CommentRanges::from(context.module().tokens()); - let source = source_text(context.db(), context.file()); - parentheses_iterator(value_node.into(), None, &comment_ranges, &source) + parentheses_iterator(value_node.into(), None, context.module().tokens()) .last() .unwrap_or(value_node.range()) } else { From f45f7d2b25edfa8868fa4c688abaab6029b4eaa1 Mon Sep 17 00:00:00 2001 From: Denys Zhak Date: Mon, 1 Dec 2025 21:16:14 +0000 Subject: [PATCH 2/7] feat: parentheses_iterator fix clippy complaint --- crates/ruff_python_parser/src/parenthesize.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_python_parser/src/parenthesize.rs b/crates/ruff_python_parser/src/parenthesize.rs index dc4d9df16e2e4..42ef026fd8937 100644 --- a/crates/ruff_python_parser/src/parenthesize.rs +++ b/crates/ruff_python_parser/src/parenthesize.rs @@ -39,7 +39,7 @@ pub fn parentheses_iterator<'a>( parent.end() } } else { - tokens.last().map_or(expr.end(), |t| t.end()) + tokens.last().map_or(expr.end(), Ranged::end) }; let right_parens = tokens From 27bbc66bce05433a3aae9d4ec9cdbb13a2f5be65 Mon Sep 17 00:00:00 2001 From: Denys Zhak Date: Tue, 2 Dec 2025 11:39:19 +0200 Subject: [PATCH 3/7] Update crates/ruff_python_parser/src/parenthesize.rs Co-authored-by: Micha Reiser --- crates/ruff_python_parser/src/parenthesize.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/ruff_python_parser/src/parenthesize.rs b/crates/ruff_python_parser/src/parenthesize.rs index 42ef026fd8937..3e29786a00d44 100644 --- a/crates/ruff_python_parser/src/parenthesize.rs +++ b/crates/ruff_python_parser/src/parenthesize.rs @@ -10,10 +10,7 @@ const fn is_trivia(kind: TokenKind) -> bool { matches!( kind, TokenKind::Comment - | TokenKind::Newline | TokenKind::NonLogicalNewline - | TokenKind::Indent - | TokenKind::Dedent ) } From 7ed3e8992a80438badcfd70c0a9d28d1de9e18b2 Mon Sep 17 00:00:00 2001 From: Denys Zhak Date: Tue, 2 Dec 2025 11:39:45 +0200 Subject: [PATCH 4/7] Update crates/ruff_python_parser/src/parenthesize.rs Co-authored-by: Micha Reiser --- crates/ruff_python_parser/src/parenthesize.rs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/ruff_python_parser/src/parenthesize.rs b/crates/ruff_python_parser/src/parenthesize.rs index 3e29786a00d44..d22445791a57c 100644 --- a/crates/ruff_python_parser/src/parenthesize.rs +++ b/crates/ruff_python_parser/src/parenthesize.rs @@ -26,25 +26,25 @@ pub fn parentheses_iterator<'a>( parent: Option, tokens: &'a Tokens, ) -> impl Iterator + 'a { - let exclusive_parent_end = if let Some(parent) = parent { + let after_tokens = if let Some(parent) = parent { // If the parent is a node that brings its own parentheses, exclude the closing parenthesis // from our search range. Otherwise, we risk matching on calls, like `func(x)`, for which // the open and close parentheses are part of the `Arguments` node. - if parent.is_arguments() { + let exclusive_parent_end = if parent.is_arguments() { parent.end() - ")".text_len() } else { parent.end() - } + }; + + tokens.in_range(TextRange::new(expr.end(), exclusive_parent_end)) } else { - tokens.last().map_or(expr.end(), Ranged::end) + tokens.after(expr.end()) }; - - let right_parens = tokens - .after(expr.end()) + + let right_parens = after_tokens .iter() - .take_while(move |token| token.start() < exclusive_parent_end) - .filter(|token| !is_trivia(token.kind())) - .take_while(|token| token.kind() == TokenKind::Rpar); + .filter(|token| !token.kind().is_trivia()) + .take_while(move |token| token.kind() == TokenKind::Rpar); let left_parens = tokens .before(expr.start()) From 9f2abce40c60d0fa89d8521c33667ed453addb3e Mon Sep 17 00:00:00 2001 From: Denys Zhak Date: Tue, 2 Dec 2025 17:30:13 +0000 Subject: [PATCH 5/7] feat: rollback to the regular TokenKind.is_trivia() --- crates/ruff_python_parser/src/parenthesize.rs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/crates/ruff_python_parser/src/parenthesize.rs b/crates/ruff_python_parser/src/parenthesize.rs index d22445791a57c..285416c86aa92 100644 --- a/crates/ruff_python_parser/src/parenthesize.rs +++ b/crates/ruff_python_parser/src/parenthesize.rs @@ -3,17 +3,6 @@ use ruff_text_size::{Ranged, TextLen, TextRange}; use crate::{TokenKind, Tokens}; -/// Tokens that should be treated as trivia when scanning around parentheses. -/// Mirrors the behavior of `SimpleTokenKind::is_trivia()` as closely as possible -/// at the `TokenKind` level. -const fn is_trivia(kind: TokenKind) -> bool { - matches!( - kind, - TokenKind::Comment - | TokenKind::NonLogicalNewline - ) -} - /// Returns an iterator over the ranges of the optional parentheses surrounding an expression. /// /// E.g. for `((f()))` with `f()` as expression, the iterator returns the ranges (1, 6) and (0, 7). @@ -40,7 +29,7 @@ pub fn parentheses_iterator<'a>( } else { tokens.after(expr.end()) }; - + let right_parens = after_tokens .iter() .filter(|token| !token.kind().is_trivia()) @@ -50,7 +39,7 @@ pub fn parentheses_iterator<'a>( .before(expr.start()) .iter() .rev() - .filter(|token| !is_trivia(token.kind())) + .filter(|token| !token.kind().is_trivia()) .take_while(|token| token.kind() == TokenKind::Lpar); right_parens From b1156a0b5239ee42695ac5ade4b315b58c904c7f Mon Sep 17 00:00:00 2001 From: Denys Zhak Date: Tue, 2 Dec 2025 17:31:28 +0000 Subject: [PATCH 6/7] feat: parenthesized_range remove integration tests with the original version --- .../tests/parenthesize_optimized.rs | 298 ------------------ 1 file changed, 298 deletions(-) delete mode 100644 crates/ruff_python_ast_integration_tests/tests/parenthesize_optimized.rs diff --git a/crates/ruff_python_ast_integration_tests/tests/parenthesize_optimized.rs b/crates/ruff_python_ast_integration_tests/tests/parenthesize_optimized.rs deleted file mode 100644 index 62f34e4bd3cc9..0000000000000 --- a/crates/ruff_python_ast_integration_tests/tests/parenthesize_optimized.rs +++ /dev/null @@ -1,298 +0,0 @@ -use ruff_python_ast::parenthesize as original_parenthesize; -use ruff_python_parser::parenthesize as optimized_parenthesize; -use ruff_python_parser::parse_expression; -use ruff_python_trivia::CommentRanges; - -#[test] -fn test_optimized_vs_original_parenthesized_name() { - let source_code = r"(x) + 1"; - let parsed = parse_expression(source_code).unwrap(); - - let bin_op = parsed.expr().as_bin_op_expr().unwrap(); - let name = bin_op.left.as_ref(); - - let original = original_parenthesize::parenthesized_range( - name.into(), - bin_op.into(), - &CommentRanges::default(), - source_code, - ); - - let optimized = - optimized_parenthesize::parenthesized_range(name.into(), bin_op.into(), parsed.tokens()); - - assert_eq!(original, optimized); - let range = optimized.expect("should find parentheses"); - assert_eq!(&source_code[range], "(x)"); -} - -#[test] -fn test_optimized_vs_original_non_parenthesized_name() { - let source_code = r"x + 1"; - let parsed = parse_expression(source_code).unwrap(); - - let bin_op = parsed.expr().as_bin_op_expr().unwrap(); - let name = bin_op.left.as_ref(); - - let original = original_parenthesize::parenthesized_range( - name.into(), - bin_op.into(), - &CommentRanges::default(), - source_code, - ); - - let optimized = - optimized_parenthesize::parenthesized_range(name.into(), bin_op.into(), parsed.tokens()); - - assert_eq!(original, optimized); - assert_eq!(optimized, None); -} - -#[test] -fn test_optimized_vs_original_parenthesized_argument() { - let source_code = r"f((a))"; - let parsed = parse_expression(source_code).unwrap(); - - let call = parsed.expr().as_call_expr().unwrap(); - let arguments = &call.arguments; - let argument = arguments.args.first().unwrap(); - - let original = original_parenthesize::parenthesized_range( - argument.into(), - arguments.into(), - &CommentRanges::default(), - source_code, - ); - - let optimized = optimized_parenthesize::parenthesized_range( - argument.into(), - arguments.into(), - parsed.tokens(), - ); - - assert_eq!(original, optimized); - let range = optimized.expect("should find parentheses"); - assert_eq!(&source_code[range], "(a)"); -} - -#[test] -fn test_optimized_vs_original_non_parenthesized_argument() { - let source_code = r"f(a)"; - let parsed = parse_expression(source_code).unwrap(); - - let call = parsed.expr().as_call_expr().unwrap(); - let arguments = &call.arguments; - let argument = arguments.args.first().unwrap(); - - let original = original_parenthesize::parenthesized_range( - argument.into(), - arguments.into(), - &CommentRanges::default(), - source_code, - ); - - let optimized = optimized_parenthesize::parenthesized_range( - argument.into(), - arguments.into(), - parsed.tokens(), - ); - - assert_eq!(original, optimized); - assert_eq!(optimized, None); -} - -#[test] -fn test_optimized_vs_original_twice_parenthesized() { - let source_code = r"((x)) + 1"; - let parsed = parse_expression(source_code).unwrap(); - - let bin_op = parsed.expr().as_bin_op_expr().unwrap(); - let name = bin_op.left.as_ref(); - - let original = original_parenthesize::parenthesized_range( - name.into(), - bin_op.into(), - &CommentRanges::default(), - source_code, - ); - - let optimized = - optimized_parenthesize::parenthesized_range(name.into(), bin_op.into(), parsed.tokens()); - - assert_eq!(original, optimized); - let range = optimized.expect("should find parentheses"); - assert_eq!(&source_code[range], "((x))"); -} - -#[test] -fn test_optimized_vs_original_with_whitespace() { - let source_code = r"( x ) + 1"; - let parsed = parse_expression(source_code).unwrap(); - - let bin_op = parsed.expr().as_bin_op_expr().unwrap(); - let name = bin_op.left.as_ref(); - - let original = original_parenthesize::parenthesized_range( - name.into(), - bin_op.into(), - &CommentRanges::default(), - source_code, - ); - - let optimized = - optimized_parenthesize::parenthesized_range(name.into(), bin_op.into(), parsed.tokens()); - - assert_eq!(original, optimized); - let range = optimized.expect("should find parentheses"); - assert_eq!(&source_code[range], "( x )"); -} - -#[test] -fn test_optimized_vs_original_with_comments() { - let source_code = r"( # comment - x -) + 1"; - let parsed = parse_expression(source_code).unwrap(); - - let bin_op = parsed.expr().as_bin_op_expr().unwrap(); - let name = bin_op.left.as_ref(); - - let comment_ranges = CommentRanges::from(parsed.tokens()); - - let original = original_parenthesize::parenthesized_range( - name.into(), - bin_op.into(), - &comment_ranges, - source_code, - ); - - let optimized = - optimized_parenthesize::parenthesized_range(name.into(), bin_op.into(), parsed.tokens()); - - assert_eq!(original, optimized); - let range = optimized.expect("should find parentheses"); - assert_eq!(&source_code[range], "( # comment\n x\n)"); -} - -#[test] -fn test_optimized_vs_original_multiple_layers() { - let source_code = r"(((x))) + 1"; - let parsed = parse_expression(source_code).unwrap(); - - let bin_op = parsed.expr().as_bin_op_expr().unwrap(); - let name = bin_op.left.as_ref(); - - let original = original_parenthesize::parenthesized_range( - name.into(), - bin_op.into(), - &CommentRanges::default(), - source_code, - ); - - let optimized = - optimized_parenthesize::parenthesized_range(name.into(), bin_op.into(), parsed.tokens()); - - assert_eq!(original, optimized); - let range = optimized.expect("should find parentheses"); - assert_eq!(&source_code[range], "(((x)))"); -} - -#[test] -fn test_optimized_vs_original_iterator_all_layers() { - let source_code = r"(((x))) + 1"; - let parsed = parse_expression(source_code).unwrap(); - - let bin_op = parsed.expr().as_bin_op_expr().unwrap(); - let name = bin_op.left.as_ref(); - - let original_layers: Vec<_> = original_parenthesize::parentheses_iterator( - name.into(), - Some(bin_op.into()), - &CommentRanges::default(), - source_code, - ) - .collect(); - - let optimized_layers: Vec<_> = optimized_parenthesize::parentheses_iterator( - name.into(), - Some(bin_op.into()), - parsed.tokens(), - ) - .collect(); - - assert_eq!(original_layers, optimized_layers); - assert_eq!(optimized_layers.len(), 3); - assert_eq!(&source_code[optimized_layers[0]], "(x)"); - assert_eq!(&source_code[optimized_layers[1]], "((x))"); - assert_eq!(&source_code[optimized_layers[2]], "(((x)))"); -} - -#[test] -fn test_optimized_vs_original_complex_expression() { - let source_code = r"((a + b) * (c - d))"; - let parsed = parse_expression(source_code).unwrap(); - - let outer_paren = parsed.expr().as_bin_op_expr().unwrap(); - - let original = original_parenthesize::parenthesized_range( - outer_paren.into(), - outer_paren.into(), - &CommentRanges::default(), - source_code, - ); - - let optimized = optimized_parenthesize::parenthesized_range( - outer_paren.into(), - outer_paren.into(), - parsed.tokens(), - ); - - assert_eq!(original, optimized); -} - -#[test] -fn test_optimized_vs_original_tuple_member() { - let source_code = r"(a, (b))"; - let parsed = parse_expression(source_code).unwrap(); - - let tuple = parsed.expr().as_tuple_expr().unwrap(); - let member = tuple.elts.last().unwrap(); - - let original = original_parenthesize::parenthesized_range( - member.into(), - tuple.into(), - &CommentRanges::default(), - source_code, - ); - - let optimized = - optimized_parenthesize::parenthesized_range(member.into(), tuple.into(), parsed.tokens()); - - assert_eq!(original, optimized); - let range = optimized.expect("should find parentheses"); - assert_eq!(&source_code[range], "(b)"); -} - -#[test] -fn test_optimized_vs_original_nested_calls() { - let source_code = r"f(g((h(x))))"; - let parsed = parse_expression(source_code).unwrap(); - - let outer_call = parsed.expr().as_call_expr().unwrap(); - let inner_arg = outer_call.arguments.args.first().unwrap(); - - let original = original_parenthesize::parenthesized_range( - inner_arg.into(), - (&outer_call.arguments).into(), - &CommentRanges::default(), - source_code, - ); - - let optimized = optimized_parenthesize::parenthesized_range( - inner_arg.into(), - (&outer_call.arguments).into(), - parsed.tokens(), - ); - - assert_eq!(original, optimized); -} From 8df704580441673b97c802a16a40949405be60f4 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 3 Dec 2025 09:08:12 +0100 Subject: [PATCH 7/7] Move to `ruff_python_ast` --- crates/ruff_python_ast/src/parenthesize.rs | 4 + crates/ruff_python_ast/src/token.rs | 2 + .../ruff_python_ast/src/token/parentheses.rs | 58 ++++ .../tests/parentheses.rs | 199 ++++++++++++++ crates/ruff_python_parser/src/lib.rs | 1 - crates/ruff_python_parser/src/parenthesize.rs | 256 ------------------ crates/ty/docs/rules.md | 149 +++++----- .../src/types/diagnostic.rs | 2 +- 8 files changed, 339 insertions(+), 332 deletions(-) create mode 100644 crates/ruff_python_ast/src/token/parentheses.rs create mode 100644 crates/ruff_python_ast_integration_tests/tests/parentheses.rs delete mode 100644 crates/ruff_python_parser/src/parenthesize.rs diff --git a/crates/ruff_python_ast/src/parenthesize.rs b/crates/ruff_python_ast/src/parenthesize.rs index a7fb1224cefa9..786ca0572cc88 100644 --- a/crates/ruff_python_ast/src/parenthesize.rs +++ b/crates/ruff_python_ast/src/parenthesize.rs @@ -11,6 +11,8 @@ use crate::ExprRef; /// Note that without a parent the range can be inaccurate, e.g. `f(a)` we falsely return a set of /// parentheses around `a` even if the parentheses actually belong to `f`. That is why you should /// generally prefer [`parenthesized_range`]. +/// +/// Prefer [`crate::token::parentheses_iterator`] if you have access to [`crate::token::Tokens`]. pub fn parentheses_iterator<'a>( expr: ExprRef<'a>, parent: Option, @@ -57,6 +59,8 @@ pub fn parentheses_iterator<'a>( /// Returns the [`TextRange`] of a given expression including parentheses, if the expression is /// parenthesized; or `None`, if the expression is not parenthesized. +/// +/// Prefer [`crate::token::parenthesized_range`] if you have access to [`crate::token::Tokens`]. pub fn parenthesized_range( expr: ExprRef, parent: AnyNodeRef, diff --git a/crates/ruff_python_ast/src/token.rs b/crates/ruff_python_ast/src/token.rs index fc1b62a36671e..4b9d98ec5c910 100644 --- a/crates/ruff_python_ast/src/token.rs +++ b/crates/ruff_python_ast/src/token.rs @@ -16,8 +16,10 @@ use crate::str_prefix::{ use crate::{AnyStringFlags, BoolOp, Operator, StringFlags, UnaryOp}; use ruff_text_size::{Ranged, TextRange}; +mod parentheses; mod tokens; +pub use parentheses::{parentheses_iterator, parenthesized_range}; pub use tokens::{TokenAt, TokenIterWithContext, Tokens}; #[derive(Clone, Copy, PartialEq, Eq)] diff --git a/crates/ruff_python_ast/src/token/parentheses.rs b/crates/ruff_python_ast/src/token/parentheses.rs new file mode 100644 index 0000000000000..c1d6f40650afa --- /dev/null +++ b/crates/ruff_python_ast/src/token/parentheses.rs @@ -0,0 +1,58 @@ +use ruff_text_size::{Ranged, TextLen, TextRange}; + +use super::{TokenKind, Tokens}; +use crate::{AnyNodeRef, ExprRef}; + +/// Returns an iterator over the ranges of the optional parentheses surrounding an expression. +/// +/// E.g. for `((f()))` with `f()` as expression, the iterator returns the ranges (1, 6) and (0, 7). +/// +/// Note that without a parent the range can be inaccurate, e.g. `f(a)` we falsely return a set of +/// parentheses around `a` even if the parentheses actually belong to `f`. That is why you should +/// generally prefer [`parenthesized_range`]. +pub fn parentheses_iterator<'a>( + expr: ExprRef<'a>, + parent: Option, + tokens: &'a Tokens, +) -> impl Iterator + 'a { + let after_tokens = if let Some(parent) = parent { + // If the parent is a node that brings its own parentheses, exclude the closing parenthesis + // from our search range. Otherwise, we risk matching on calls, like `func(x)`, for which + // the open and close parentheses are part of the `Arguments` node. + let exclusive_parent_end = if parent.is_arguments() { + parent.end() - ")".text_len() + } else { + parent.end() + }; + + tokens.in_range(TextRange::new(expr.end(), exclusive_parent_end)) + } else { + tokens.after(expr.end()) + }; + + let right_parens = after_tokens + .iter() + .filter(|token| !token.kind().is_trivia()) + .take_while(move |token| token.kind() == TokenKind::Rpar); + + let left_parens = tokens + .before(expr.start()) + .iter() + .rev() + .filter(|token| !token.kind().is_trivia()) + .take_while(|token| token.kind() == TokenKind::Lpar); + + right_parens + .zip(left_parens) + .map(|(right, left)| TextRange::new(left.start(), right.end())) +} + +/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is +/// parenthesized; or `None`, if the expression is not parenthesized. +pub fn parenthesized_range( + expr: ExprRef, + parent: AnyNodeRef, + tokens: &Tokens, +) -> Option { + parentheses_iterator(expr, Some(parent), tokens).last() +} diff --git a/crates/ruff_python_ast_integration_tests/tests/parentheses.rs b/crates/ruff_python_ast_integration_tests/tests/parentheses.rs new file mode 100644 index 0000000000000..479c456c39339 --- /dev/null +++ b/crates/ruff_python_ast_integration_tests/tests/parentheses.rs @@ -0,0 +1,199 @@ +//! Tests for [`ruff_python_ast::tokens::parentheses_iterator`] and +//! [`ruff_python_ast::tokens::parenthesized_range`]. + +use ruff_python_ast::{ + self as ast, Expr, + token::{parentheses_iterator, parenthesized_range}, +}; +use ruff_python_parser::parse_module; + +#[test] +fn test_no_parentheses() { + let source = "x = 2 + 2"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + assert_eq!(result, None); +} + +#[test] +fn test_single_parentheses() { + let source = "x = (2 + 2)"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + let range = result.expect("should find parentheses"); + assert_eq!(&source[range], "(2 + 2)"); +} + +#[test] +fn test_double_parentheses() { + let source = "x = ((2 + 2))"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + let range = result.expect("should find parentheses"); + assert_eq!(&source[range], "((2 + 2))"); +} + +#[test] +fn test_parentheses_with_whitespace() { + let source = "x = ( 2 + 2 )"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + let range = result.expect("should find parentheses"); + assert_eq!(&source[range], "( 2 + 2 )"); +} + +#[test] +fn test_parentheses_with_comments() { + let source = "x = ( # comment\n 2 + 2\n)"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + let range = result.expect("should find parentheses"); + assert_eq!(&source[range], "( # comment\n 2 + 2\n)"); +} + +#[test] +fn test_parenthesized_range_multiple() { + let source = "x = (((2 + 2)))"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + let range = result.expect("should find parentheses"); + assert_eq!(&source[range], "(((2 + 2)))"); +} + +#[test] +fn test_parentheses_iterator_multiple() { + let source = "x = (((2 + 2)))"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let ranges: Vec<_> = + parentheses_iterator(assign.value.as_ref().into(), Some(stmt.into()), tokens).collect(); + assert_eq!(ranges.len(), 3); + assert_eq!(&source[ranges[0]], "(2 + 2)"); + assert_eq!(&source[ranges[1]], "((2 + 2))"); + assert_eq!(&source[ranges[2]], "(((2 + 2)))"); +} + +#[test] +fn test_call_arguments_not_counted() { + let source = "f(x)"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Expr(expr_stmt) = stmt else { + panic!("expected `Expr` statement, got {stmt:?}"); + }; + + let Expr::Call(call) = expr_stmt.value.as_ref() else { + panic!("expected Call expression, got {:?}", expr_stmt.value); + }; + + let arg = call + .arguments + .args + .first() + .expect("call should have an argument"); + let result = parenthesized_range(arg.into(), (&call.arguments).into(), tokens); + // The parentheses belong to the call, not the argument + assert_eq!(result, None); +} + +#[test] +fn test_call_with_parenthesized_argument() { + let source = "f((x))"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Expr(expr_stmt) = stmt else { + panic!("expected Expr statement, got {stmt:?}"); + }; + + let Expr::Call(call) = expr_stmt.value.as_ref() else { + panic!("expected `Call` expression, got {:?}", expr_stmt.value); + }; + + let arg = call + .arguments + .args + .first() + .expect("call should have an argument"); + let result = parenthesized_range(arg.into(), (&call.arguments).into(), tokens); + + let range = result.expect("should find parentheses around argument"); + assert_eq!(&source[range], "(x)"); +} + +#[test] +fn test_multiline_with_parentheses() { + let source = "x = (\n 2 + 2 + 2\n)"; + let parsed = parse_module(source).expect("should parse valid python"); + let tokens = parsed.tokens(); + let module = parsed.syntax(); + + let stmt = module.body.first().expect("module should have a statement"); + let ast::Stmt::Assign(assign) = stmt else { + panic!("expected `Assign` statement, got {stmt:?}"); + }; + + let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); + let range = result.expect("should find parentheses"); + assert_eq!(&source[range], "(\n 2 + 2 + 2\n)"); +} diff --git a/crates/ruff_python_parser/src/lib.rs b/crates/ruff_python_parser/src/lib.rs index 83799d7b0c2de..86bfe5669740c 100644 --- a/crates/ruff_python_parser/src/lib.rs +++ b/crates/ruff_python_parser/src/lib.rs @@ -80,7 +80,6 @@ use ruff_text_size::{Ranged, TextRange}; mod error; pub mod lexer; -pub mod parenthesize; mod parser; pub mod semantic_errors; mod string; diff --git a/crates/ruff_python_parser/src/parenthesize.rs b/crates/ruff_python_parser/src/parenthesize.rs deleted file mode 100644 index 285416c86aa92..0000000000000 --- a/crates/ruff_python_parser/src/parenthesize.rs +++ /dev/null @@ -1,256 +0,0 @@ -use ruff_python_ast::{AnyNodeRef, ExprRef}; -use ruff_text_size::{Ranged, TextLen, TextRange}; - -use crate::{TokenKind, Tokens}; - -/// Returns an iterator over the ranges of the optional parentheses surrounding an expression. -/// -/// E.g. for `((f()))` with `f()` as expression, the iterator returns the ranges (1, 6) and (0, 7). -/// -/// Note that without a parent the range can be inaccurate, e.g. `f(a)` we falsely return a set of -/// parentheses around `a` even if the parentheses actually belong to `f`. That is why you should -/// generally prefer [`parenthesized_range`]. -pub fn parentheses_iterator<'a>( - expr: ExprRef<'a>, - parent: Option, - tokens: &'a Tokens, -) -> impl Iterator + 'a { - let after_tokens = if let Some(parent) = parent { - // If the parent is a node that brings its own parentheses, exclude the closing parenthesis - // from our search range. Otherwise, we risk matching on calls, like `func(x)`, for which - // the open and close parentheses are part of the `Arguments` node. - let exclusive_parent_end = if parent.is_arguments() { - parent.end() - ")".text_len() - } else { - parent.end() - }; - - tokens.in_range(TextRange::new(expr.end(), exclusive_parent_end)) - } else { - tokens.after(expr.end()) - }; - - let right_parens = after_tokens - .iter() - .filter(|token| !token.kind().is_trivia()) - .take_while(move |token| token.kind() == TokenKind::Rpar); - - let left_parens = tokens - .before(expr.start()) - .iter() - .rev() - .filter(|token| !token.kind().is_trivia()) - .take_while(|token| token.kind() == TokenKind::Lpar); - - right_parens - .zip(left_parens) - .map(|(right, left)| TextRange::new(left.start(), right.end())) -} - -/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is -/// parenthesized; or `None`, if the expression is not parenthesized. -pub fn parenthesized_range( - expr: ExprRef, - parent: AnyNodeRef, - tokens: &Tokens, -) -> Option { - parentheses_iterator(expr, Some(parent), tokens).last() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::parse_module; - use ruff_python_ast::{self as ast, Expr}; - - #[test] - fn test_no_parentheses() { - let source = "x = 2 + 2"; - let parsed = parse_module(source).expect("should parse valid python"); - let tokens = parsed.tokens(); - let module = parsed.syntax(); - - let stmt = module.body.first().expect("module should have a statement"); - let ast::Stmt::Assign(assign) = stmt else { - panic!("expected `Assign` statement, got {stmt:?}"); - }; - - let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); - assert_eq!(result, None); - } - - #[test] - fn test_single_parentheses() { - let source = "x = (2 + 2)"; - let parsed = parse_module(source).expect("should parse valid python"); - let tokens = parsed.tokens(); - let module = parsed.syntax(); - - let stmt = module.body.first().expect("module should have a statement"); - let ast::Stmt::Assign(assign) = stmt else { - panic!("expected `Assign` statement, got {stmt:?}"); - }; - - let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); - let range = result.expect("should find parentheses"); - assert_eq!(&source[range], "(2 + 2)"); - } - - #[test] - fn test_double_parentheses() { - let source = "x = ((2 + 2))"; - let parsed = parse_module(source).expect("should parse valid python"); - let tokens = parsed.tokens(); - let module = parsed.syntax(); - - let stmt = module.body.first().expect("module should have a statement"); - let ast::Stmt::Assign(assign) = stmt else { - panic!("expected `Assign` statement, got {stmt:?}"); - }; - - let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); - let range = result.expect("should find parentheses"); - assert_eq!(&source[range], "((2 + 2))"); - } - - #[test] - fn test_parentheses_with_whitespace() { - let source = "x = ( 2 + 2 )"; - let parsed = parse_module(source).expect("should parse valid python"); - let tokens = parsed.tokens(); - let module = parsed.syntax(); - - let stmt = module.body.first().expect("module should have a statement"); - let ast::Stmt::Assign(assign) = stmt else { - panic!("expected `Assign` statement, got {stmt:?}"); - }; - - let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); - let range = result.expect("should find parentheses"); - assert_eq!(&source[range], "( 2 + 2 )"); - } - - #[test] - fn test_parentheses_with_comments() { - let source = "x = ( # comment\n 2 + 2\n)"; - let parsed = parse_module(source).expect("should parse valid python"); - let tokens = parsed.tokens(); - let module = parsed.syntax(); - - let stmt = module.body.first().expect("module should have a statement"); - let ast::Stmt::Assign(assign) = stmt else { - panic!("expected `Assign` statement, got {stmt:?}"); - }; - - let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); - let range = result.expect("should find parentheses"); - assert_eq!(&source[range], "( # comment\n 2 + 2\n)"); - } - - #[test] - fn test_parenthesized_range_multiple() { - let source = "x = (((2 + 2)))"; - let parsed = parse_module(source).expect("should parse valid python"); - let tokens = parsed.tokens(); - let module = parsed.syntax(); - - let stmt = module.body.first().expect("module should have a statement"); - let ast::Stmt::Assign(assign) = stmt else { - panic!("expected `Assign` statement, got {stmt:?}"); - }; - - let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); - let range = result.expect("should find parentheses"); - assert_eq!(&source[range], "(((2 + 2)))"); - } - - #[test] - fn test_parentheses_iterator_multiple() { - let source = "x = (((2 + 2)))"; - let parsed = parse_module(source).expect("should parse valid python"); - let tokens = parsed.tokens(); - let module = parsed.syntax(); - - let stmt = module.body.first().expect("module should have a statement"); - let ast::Stmt::Assign(assign) = stmt else { - panic!("expected `Assign` statement, got {stmt:?}"); - }; - - let ranges: Vec<_> = - parentheses_iterator(assign.value.as_ref().into(), Some(stmt.into()), tokens).collect(); - assert_eq!(ranges.len(), 3); - assert_eq!(&source[ranges[0]], "(2 + 2)"); - assert_eq!(&source[ranges[1]], "((2 + 2))"); - assert_eq!(&source[ranges[2]], "(((2 + 2)))"); - } - - #[test] - fn test_call_arguments_not_counted() { - let source = "f(x)"; - let parsed = parse_module(source).expect("should parse valid python"); - let tokens = parsed.tokens(); - let module = parsed.syntax(); - - let stmt = module.body.first().expect("module should have a statement"); - let ast::Stmt::Expr(expr_stmt) = stmt else { - panic!("expected `Expr` statement, got {stmt:?}"); - }; - - let Expr::Call(call) = expr_stmt.value.as_ref() else { - panic!("expected Call expression, got {:?}", expr_stmt.value); - }; - - let arg = call - .arguments - .args - .first() - .expect("call should have an argument"); - let result = parenthesized_range(arg.into(), (&call.arguments).into(), tokens); - // The parentheses belong to the call, not the argument - assert_eq!(result, None); - } - - #[test] - fn test_call_with_parenthesized_argument() { - let source = "f((x))"; - let parsed = parse_module(source).expect("should parse valid python"); - let tokens = parsed.tokens(); - let module = parsed.syntax(); - - let stmt = module.body.first().expect("module should have a statement"); - let ast::Stmt::Expr(expr_stmt) = stmt else { - panic!("expected Expr statement, got {stmt:?}"); - }; - - let Expr::Call(call) = expr_stmt.value.as_ref() else { - panic!("expected `Call` expression, got {:?}", expr_stmt.value); - }; - - let arg = call - .arguments - .args - .first() - .expect("call should have an argument"); - let result = parenthesized_range(arg.into(), (&call.arguments).into(), tokens); - - let range = result.expect("should find parentheses around argument"); - assert_eq!(&source[range], "(x)"); - } - - #[test] - fn test_multiline_with_parentheses() { - let source = "x = (\n 2 + 2 + 2\n)"; - let parsed = parse_module(source).expect("should parse valid python"); - let tokens = parsed.tokens(); - let module = parsed.syntax(); - - let stmt = module.body.first().expect("module should have a statement"); - let ast::Stmt::Assign(assign) = stmt else { - panic!("expected `Assign` statement, got {stmt:?}"); - }; - - let result = parenthesized_range(assign.value.as_ref().into(), stmt.into(), tokens); - let range = result.expect("should find parentheses"); - assert_eq!(&source[range], "(\n 2 + 2 + 2\n)"); - } -} diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index c4a20fe89cd89..5ac36c4fb959a 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -39,7 +39,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -95,7 +95,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -126,7 +126,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -158,7 +158,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -190,7 +190,7 @@ class B(A): ... Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -218,7 +218,7 @@ type B = A Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -245,7 +245,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -357,7 +357,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -387,7 +387,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -413,7 +413,7 @@ t[3] # IndexError: tuple index out of range Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -502,7 +502,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -557,7 +557,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -591,7 +591,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -627,7 +627,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -678,7 +678,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -707,7 +707,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -751,7 +751,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -793,7 +793,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -826,7 +826,7 @@ class C[U](Generic[T]): ... Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -865,7 +865,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -900,7 +900,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -934,7 +934,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1041,7 +1041,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1095,7 +1095,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -1125,7 +1125,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1175,7 +1175,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1201,7 +1201,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1232,7 +1232,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1266,7 +1266,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1315,7 +1315,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1340,7 +1340,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1398,7 +1398,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1425,7 +1425,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1472,7 +1472,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1502,7 +1502,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1532,7 +1532,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1566,7 +1566,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1600,7 +1600,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1635,7 +1635,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1660,7 +1660,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1693,7 +1693,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1722,7 +1722,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1746,7 +1746,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1772,7 +1772,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1805,7 +1805,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1832,7 +1832,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1890,7 +1890,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1920,7 +1920,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1949,7 +1949,7 @@ class B(A): ... # Error raised here Default level: error · Preview (since 0.0.1-alpha.30) · Related issues · -View source +View source @@ -1983,7 +1983,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2010,7 +2010,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2038,7 +2038,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2084,7 +2084,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2111,7 +2111,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2139,7 +2139,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2164,7 +2164,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2189,7 +2189,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2226,7 +2226,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2254,7 +2254,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2279,7 +2279,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2320,7 +2320,7 @@ class SubProto(BaseProto, Protocol): Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -2408,7 +2408,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2436,7 +2436,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2468,7 +2468,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2500,7 +2500,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2527,7 +2527,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2551,7 +2551,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2609,7 +2609,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2648,7 +2648,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2711,7 +2711,7 @@ def foo(x: int | str) -> int | str: Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -2735,7 +2735,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2787,3 +2787,4 @@ Use instead: ```py a = 20 / 2 ``` + diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index ce5788148978a..d72cbf8dbcd84 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -40,8 +40,8 @@ use ruff_db::{ }; use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::name::Name; +use ruff_python_ast::token::parentheses_iterator; use ruff_python_ast::{self as ast, AnyNodeRef, StringFlags}; -use ruff_python_parser::parenthesize::parentheses_iterator; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; use std::fmt::{self, Formatter};