diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 993ecfac97..b218a2e99b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -298,7 +298,7 @@ jobs: # sync, not just public items. Eventually we should do this for all # crates; for now add crates here as they are warning-clean to prevent # regression. - - run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db --document-private-items + - run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db -p ruff_python_formatter --document-private-items env: # Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025). RUSTDOCFLAGS: "-D warnings" diff --git a/Cargo.lock b/Cargo.lock index d0018e76e3..b6ca38375b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3349,6 +3349,7 @@ dependencies = [ "compact_str", "get-size2", "insta", + "itertools 0.14.0", "memchr", "ruff_annotate_snippets", "ruff_python_ast", diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 4d4d7e9293..37422cbf18 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -437,6 +437,15 @@ impl<'a> Checker<'a> { } } + /// Returns the [`Tokens`] for the parsed source file. + /// + /// + /// Unlike [`Self::tokens`], this method always returns + /// the tokens for the current file, even when within a parsed type annotation. + pub(crate) fn source_tokens(&self) -> &'a Tokens { + self.parsed.tokens() + } + /// The [`Locator`] for the current file, which enables extraction of source code from byte /// offsets. pub(crate) const fn locator(&self) -> &'a Locator<'a> { diff --git a/crates/ruff_linter/src/fix/edits.rs b/crates/ruff_linter/src/fix/edits.rs index 05e90519eb..20f50d6e10 100644 --- a/crates/ruff_linter/src/fix/edits.rs +++ b/crates/ruff_linter/src/fix/edits.rs @@ -3,14 +3,13 @@ use anyhow::{Context, Result}; use ruff_python_ast::AnyNodeRef; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::{self, Tokens, parenthesized_range}; use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Parameters, Stmt}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_trivia::textwrap::dedent_to; use ruff_python_trivia::{ - CommentRanges, PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content, - is_python_whitespace, + PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content, is_python_whitespace, }; use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlines}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -209,7 +208,7 @@ pub(crate) fn remove_argument( arguments: &Arguments, parentheses: Parentheses, source: &str, - comment_ranges: &CommentRanges, + tokens: &Tokens, ) -> Result { // Partition into arguments before and after the argument to remove. let (before, after): (Vec<_>, Vec<_>) = arguments @@ -224,7 +223,7 @@ pub(crate) fn remove_argument( .context("Unable to find argument")?; let parenthesized_range = - parenthesized_range(arg.value().into(), arguments.into(), comment_ranges, source) + token::parenthesized_range(arg.value().into(), arguments.into(), tokens) .unwrap_or(arg.range()); if !after.is_empty() { @@ -270,25 +269,14 @@ pub(crate) fn remove_argument( /// /// The new argument will be inserted before the first existing keyword argument in `arguments`, if /// there are any present. Otherwise, the new argument is added to the end of the argument list. -pub(crate) fn add_argument( - argument: &str, - arguments: &Arguments, - comment_ranges: &CommentRanges, - source: &str, -) -> Edit { +pub(crate) fn add_argument(argument: &str, arguments: &Arguments, tokens: &Tokens) -> Edit { if let Some(ast::Keyword { range, value, .. }) = arguments.keywords.first() { - let keyword = parenthesized_range(value.into(), arguments.into(), comment_ranges, source) - .unwrap_or(*range); + let keyword = parenthesized_range(value.into(), arguments.into(), tokens).unwrap_or(*range); Edit::insertion(format!("{argument}, "), keyword.start()) } else if let Some(last) = arguments.arguments_source_order().last() { // Case 1: existing arguments, so append after the last argument. - let last = parenthesized_range( - last.value().into(), - arguments.into(), - comment_ranges, - source, - ) - .unwrap_or(last.range()); + let last = parenthesized_range(last.value().into(), arguments.into(), tokens) + .unwrap_or(last.range()); Edit::insertion(format!(", {argument}"), last.end()) } else { // Case 2: no arguments. Add argument, without any trailing comma. diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs index 440be901a7..d31ffbf61e 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs @@ -91,8 +91,8 @@ pub(crate) fn fastapi_redundant_response_model(checker: &Checker, function_def: response_model_arg, &call.arguments, Parentheses::Preserve, - checker.locator().contents(), - checker.comment_ranges(), + checker.source(), + checker.tokens(), ) .map(Fix::unsafe_edit) }); diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs index cd268f610b..04e9640e09 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs @@ -74,12 +74,7 @@ pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCal checker .report_diagnostic(MapWithoutExplicitStrict, call.range()) .set_fix(Fix::applicable_edit( - add_argument( - "strict=False", - &call.arguments, - checker.comment_ranges(), - checker.locator().contents(), - ), + add_argument("strict=False", &call.arguments, checker.tokens()), Applicability::Unsafe, )); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs index 94098831b8..c0b1b5e840 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs @@ -3,7 +3,7 @@ use std::fmt::Write; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_docstring_stmt; use ruff_python_ast::name::QualifiedName; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr, ParameterWithDefault}; use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::function_type::is_stub; @@ -166,12 +166,7 @@ fn move_initialization( return None; } - let range = match parenthesized_range( - default.into(), - parameter.into(), - checker.comment_ranges(), - checker.source(), - ) { + let range = match parenthesized_range(default.into(), parameter.into(), checker.tokens()) { Some(range) => range, None => default.range(), }; @@ -194,13 +189,8 @@ fn move_initialization( "{} = {}", parameter.parameter.name(), locator.slice( - parenthesized_range( - default.into(), - parameter.into(), - checker.comment_ranges(), - checker.source() - ) - .unwrap_or(default.range()) + parenthesized_range(default.into(), parameter.into(), checker.tokens()) + .unwrap_or(default.range()) ) ); } else { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs index f737e781ef..a21cf9a30e 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs @@ -92,12 +92,7 @@ pub(crate) fn no_explicit_stacklevel(checker: &Checker, call: &ast::ExprCall) { } let mut diagnostic = checker.report_diagnostic(NoExplicitStacklevel, call.func.range()); - let edit = add_argument( - "stacklevel=2", - &call.arguments, - checker.comment_ranges(), - checker.locator().contents(), - ); + let edit = add_argument("stacklevel=2", &call.arguments, checker.tokens()); diagnostic.set_fix(Fix::unsafe_edit(edit)); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs index 136715b981..db71c7b2fb 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs @@ -70,12 +70,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal checker .report_diagnostic(ZipWithoutExplicitStrict, call.range()) .set_fix(Fix::applicable_edit( - add_argument( - "strict=False", - &call.arguments, - checker.comment_ranges(), - checker.locator().contents(), - ), + add_argument("strict=False", &call.arguments, checker.tokens()), Applicability::Unsafe, )); } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs index 5fdc1a37a3..d271a13792 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs @@ -2,8 +2,8 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::ExprGenerator; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::TokenKind; +use ruff_python_ast::token::parenthesized_range; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; @@ -142,13 +142,9 @@ pub(crate) fn unnecessary_generator_list(checker: &Checker, call: &ast::ExprCall if *parenthesized { // The generator's range will include the innermost parentheses, but it could be // surrounded by additional parentheses. - let range = parenthesized_range( - argument.into(), - (&call.arguments).into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(argument.range()); + let range = + parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens()) + .unwrap_or(argument.range()); // The generator always parenthesizes the expression; trim the parentheses. let generator = checker.generator().expr(argument); diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs index 0560935bae..05a1c523cf 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs @@ -2,8 +2,8 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::ExprGenerator; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::TokenKind; +use ruff_python_ast::token::parenthesized_range; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; @@ -147,13 +147,9 @@ pub(crate) fn unnecessary_generator_set(checker: &Checker, call: &ast::ExprCall) if *parenthesized { // The generator's range will include the innermost parentheses, but it could be // surrounded by additional parentheses. - let range = parenthesized_range( - argument.into(), - (&call.arguments).into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(argument.range()); + let range = + parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens()) + .unwrap_or(argument.range()); // The generator always parenthesizes the expression; trim the parentheses. let generator = checker.generator().expr(argument); diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs index b4fda738e2..f6699500af 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs @@ -1,7 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; -use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::TokenKind; +use ruff_python_ast::token::parenthesized_range; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; @@ -89,13 +89,9 @@ pub(crate) fn unnecessary_list_comprehension_set(checker: &Checker, call: &ast:: // If the list comprehension is parenthesized, remove the parentheses in addition to // removing the brackets. - let replacement_range = parenthesized_range( - argument.into(), - (&call.arguments).into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or_else(|| argument.range()); + let replacement_range = + parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens()) + .unwrap_or_else(|| argument.range()); let span = argument.range().add_start(one).sub_end(one); let replacement = diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs index 8edb7b46ad..e742b8922d 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs @@ -1,5 +1,5 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr, Operator}; use ruff_python_trivia::is_python_whitespace; use ruff_source_file::LineRanges; @@ -88,13 +88,7 @@ pub(crate) fn explicit(checker: &Checker, expr: &Expr) { checker.report_diagnostic(ExplicitStringConcatenation, expr.range()); let is_parenthesized = |expr: &Expr| { - parenthesized_range( - expr.into(), - bin_op.into(), - checker.comment_ranges(), - checker.source(), - ) - .is_some() + parenthesized_range(expr.into(), bin_op.into(), checker.tokens()).is_some() }; // If either `left` or `right` is parenthesized, generating // a fix would be too involved. Just report the diagnostic. diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs index 1172e40893..7d18897708 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs @@ -111,7 +111,6 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall } let arguments = &call.arguments; - let source = checker.source(); let mut diagnostic = checker.report_diagnostic(ExcInfoOutsideExceptHandler, exc_info.range); @@ -120,8 +119,8 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall exc_info, arguments, Parentheses::Preserve, - source, - checker.comment_ranges(), + checker.source(), + checker.tokens(), )?; Ok(Fix::unsafe_edit(edit)) }); diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs index cc436d258a..1b3faba504 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use rustc_hash::{FxBuildHasher, FxHashSet}; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; use ruff_python_stdlib::identifiers::is_identifier; use ruff_text_size::Ranged; @@ -129,8 +129,8 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) { keyword, &call.arguments, Parentheses::Preserve, - checker.locator().contents(), - checker.comment_ranges(), + checker.source(), + checker.tokens(), ) .map(Fix::safe_edit) }); @@ -158,8 +158,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) { parenthesized_range( value.into(), dict.into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens() ) .unwrap_or(value.range()) ) diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs index 6e6b10b206..e12af6e069 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs @@ -73,11 +73,11 @@ pub(crate) fn unnecessary_range_start(checker: &Checker, call: &ast::ExprCall) { let mut diagnostic = checker.report_diagnostic(UnnecessaryRangeStart, start.range()); diagnostic.try_set_fix(|| { remove_argument( - &start, + start, &call.arguments, Parentheses::Preserve, - checker.locator().contents(), - checker.comment_ranges(), + checker.source(), + checker.tokens(), ) .map(Fix::safe_edit) }); diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs index 4cd5035693..90b8476809 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs @@ -160,20 +160,16 @@ fn generate_fix( ) -> anyhow::Result { let locator = checker.locator(); let source = locator.contents(); + let tokens = checker.tokens(); let deletion = remove_argument( generic_base, arguments, Parentheses::Preserve, source, - checker.comment_ranges(), + tokens, )?; - let insertion = add_argument( - locator.slice(generic_base), - arguments, - checker.comment_ranges(), - source, - ); + let insertion = add_argument(locator.slice(generic_base), arguments, tokens); Ok(Fix::unsafe_edits(deletion, [insertion])) } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 14145229fc..b3e35c21c2 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -5,7 +5,7 @@ use ruff_python_ast::{ helpers::{pep_604_union, typing_optional}, name::Name, operator_precedence::OperatorPrecedence, - parenthesize::parenthesized_range, + token::{Tokens, parenthesized_range}, }; use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union}; use ruff_text_size::{Ranged, TextRange}; @@ -243,16 +243,12 @@ fn create_fix( let union_expr = pep_604_union(&[new_literal_expr, none_expr]); // Check if we need parentheses to preserve operator precedence - let content = if needs_parentheses_for_precedence( - semantic, - literal_expr, - checker.comment_ranges(), - checker.source(), - ) { - format!("({})", checker.generator().expr(&union_expr)) - } else { - checker.generator().expr(&union_expr) - }; + let content = + if needs_parentheses_for_precedence(semantic, literal_expr, checker.tokens()) { + format!("({})", checker.generator().expr(&union_expr)) + } else { + checker.generator().expr(&union_expr) + }; let union_edit = Edit::range_replacement(content, literal_expr.range()); Fix::applicable_edit(union_edit, applicability) @@ -278,8 +274,7 @@ enum UnionKind { fn needs_parentheses_for_precedence( semantic: &ruff_python_semantic::SemanticModel, literal_expr: &Expr, - comment_ranges: &ruff_python_trivia::CommentRanges, - source: &str, + tokens: &Tokens, ) -> bool { // Get the parent expression to check if we're in a context that needs parentheses let Some(parent_expr) = semantic.current_expression_parent() else { @@ -287,14 +282,7 @@ fn needs_parentheses_for_precedence( }; // Check if the literal expression is already parenthesized - if parenthesized_range( - literal_expr.into(), - parent_expr.into(), - comment_ranges, - source, - ) - .is_some() - { + if parenthesized_range(literal_expr.into(), parent_expr.into(), tokens).is_some() { return false; // Already parenthesized, don't add more } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs index 545372dd6c..c97efd8b05 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs @@ -10,7 +10,7 @@ use libcst_native::{ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::Truthiness; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{ self as ast, AnyNodeRef, Arguments, BoolOp, ExceptHandler, Expr, Keyword, Stmt, UnaryOp, @@ -303,8 +303,7 @@ pub(crate) fn unittest_assertion( parenthesized_range( expr.into(), checker.semantic().current_statement().into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), ) .unwrap_or(expr.range()), ))); diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs index 49be564b48..c939d44346 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs @@ -768,8 +768,8 @@ fn check_fixture_decorator(checker: &Checker, func_name: &str, decorator: &Decor keyword, arguments, edits::Parentheses::Preserve, - checker.locator().contents(), - checker.comment_ranges(), + checker.source(), + checker.tokens(), ) .map(Fix::unsafe_edit) }); diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs index 20b14399f9..904ca1c494 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs @@ -2,10 +2,9 @@ use rustc_hash::{FxBuildHasher, FxHashMap}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::{Tokens, parenthesized_range}; use ruff_python_ast::{self as ast, Expr, ExprCall, ExprContext, StringLiteralFlags}; use ruff_python_codegen::Generator; -use ruff_python_trivia::CommentRanges; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -322,18 +321,8 @@ fn elts_to_csv(elts: &[Expr], generator: Generator, flags: StringLiteralFlags) - /// ``` /// /// This method assumes that the first argument is a string. -fn get_parametrize_name_range( - call: &ExprCall, - expr: &Expr, - comment_ranges: &CommentRanges, - source: &str, -) -> Option { - parenthesized_range( - expr.into(), - (&call.arguments).into(), - comment_ranges, - source, - ) +fn get_parametrize_name_range(call: &ExprCall, expr: &Expr, tokens: &Tokens) -> Option { + parenthesized_range(expr.into(), (&call.arguments).into(), tokens) } /// PT006 @@ -349,13 +338,8 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr if names.len() > 1 { match names_type { types::ParametrizeNameType::Tuple => { - let name_range = get_parametrize_name_range( - call, - expr, - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(expr.range()); + let name_range = get_parametrize_name_range(call, expr, checker.tokens()) + .unwrap_or(expr.range()); let mut diagnostic = checker.report_diagnostic( PytestParametrizeNamesWrongType { single_argument: false, @@ -386,13 +370,8 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr ))); } types::ParametrizeNameType::List => { - let name_range = get_parametrize_name_range( - call, - expr, - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(expr.range()); + let name_range = get_parametrize_name_range(call, expr, checker.tokens()) + .unwrap_or(expr.range()); let mut diagnostic = checker.report_diagnostic( PytestParametrizeNamesWrongType { single_argument: false, diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs index 0b53a271f4..5de313a600 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs @@ -10,7 +10,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::{Truthiness, contains_effect}; use ruff_python_ast::name::Name; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_codegen::Generator; use ruff_python_semantic::SemanticModel; @@ -800,14 +800,9 @@ fn is_short_circuit( edit = Some(get_short_circuit_edit( value, TextRange::new( - parenthesized_range( - furthest.into(), - expr.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(furthest.range()) - .start(), + parenthesized_range(furthest.into(), expr.into(), checker.tokens()) + .unwrap_or(furthest.range()) + .start(), expr.end(), ), short_circuit_truthiness, @@ -828,14 +823,9 @@ fn is_short_circuit( edit = Some(get_short_circuit_edit( next_value, TextRange::new( - parenthesized_range( - furthest.into(), - expr.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(furthest.range()) - .start(), + parenthesized_range(furthest.into(), expr.into(), checker.tokens()) + .unwrap_or(furthest.range()) + .start(), expr.end(), ), short_circuit_truthiness, diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs index 15b2f54bf8..2292f4e581 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs @@ -4,7 +4,7 @@ use ruff_text_size::{Ranged, TextRange}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::{is_const_false, is_const_true}; use ruff_python_ast::name::Name; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use crate::checkers::ast::Checker; use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; @@ -171,13 +171,8 @@ pub(crate) fn if_expr_with_true_false( checker .locator() .slice( - parenthesized_range( - test.into(), - expr.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(test.range()), + parenthesized_range(test.into(), expr.into(), checker.tokens()) + .unwrap_or(test.range()), ) .to_string(), expr.range(), diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs index 92182b383a..17b7c5c612 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs @@ -4,10 +4,10 @@ use anyhow::Result; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableStmt; -use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::stmt_if::{IfElifBranch, if_elif_branches}; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; -use ruff_python_trivia::{CommentRanges, SimpleTokenKind, SimpleTokenizer}; +use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; @@ -99,7 +99,7 @@ pub(crate) fn if_with_same_arms(checker: &Checker, stmt_if: &ast::StmtIf) { ¤t_branch, following_branch, checker.locator(), - checker.comment_ranges(), + checker.tokens(), ) }); } @@ -111,7 +111,7 @@ fn merge_branches( current_branch: &IfElifBranch, following_branch: &IfElifBranch, locator: &Locator, - comment_ranges: &CommentRanges, + tokens: &ruff_python_ast::token::Tokens, ) -> Result { // Identify the colon (`:`) at the end of the current branch's test. let Some(current_branch_colon) = @@ -127,12 +127,9 @@ fn merge_branches( ); // If the following test isn't parenthesized, consider parenthesizing it. - let following_branch_test = if let Some(range) = parenthesized_range( - following_branch.test.into(), - stmt_if.into(), - comment_ranges, - locator.contents(), - ) { + let following_branch_test = if let Some(range) = + parenthesized_range(following_branch.test.into(), stmt_if.into(), tokens) + { Cow::Borrowed(locator.slice(range)) } else if matches!( following_branch.test, @@ -153,24 +150,19 @@ fn merge_branches( // // For example, if the current test is `x if x else y`, we should parenthesize it to // `(x if x else y) or ...`. - let parenthesize_edit = if matches!( - current_branch.test, - Expr::Lambda(_) | Expr::Named(_) | Expr::If(_) - ) && parenthesized_range( - current_branch.test.into(), - stmt_if.into(), - comment_ranges, - locator.contents(), - ) - .is_none() - { - Some(Edit::range_replacement( - format!("({})", locator.slice(current_branch.test)), - current_branch.test.range(), - )) - } else { - None - }; + let parenthesize_edit = + if matches!( + current_branch.test, + Expr::Lambda(_) | Expr::Named(_) | Expr::If(_) + ) && parenthesized_range(current_branch.test.into(), stmt_if.into(), tokens).is_none() + { + Some(Edit::range_replacement( + format!("({})", locator.slice(current_branch.test)), + current_branch.test.range(), + )) + } else { + None + }; Ok(Fix::safe_edits( deletion_edit, diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs index 5ced08a673..645a79d9e1 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs @@ -1,6 +1,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::AnyNodeRef; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Arguments, CmpOp, Comprehension, Expr}; use ruff_python_semantic::analyze::typing; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; @@ -90,20 +90,10 @@ fn key_in_dict(checker: &Checker, left: &Expr, right: &Expr, operator: CmpOp, pa } // Extract the exact range of the left and right expressions. - let left_range = parenthesized_range( - left.into(), - parent, - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(left.range()); - let right_range = parenthesized_range( - right.into(), - parent, - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(right.range()); + let left_range = + parenthesized_range(left.into(), parent, checker.tokens()).unwrap_or(left.range()); + let right_range = + parenthesized_range(right.into(), parent, checker.tokens()).unwrap_or(right.range()); let mut diagnostic = checker.report_diagnostic( InDictKeys { diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index 776ce1486e..c07de4b813 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -11,7 +11,7 @@ use crate::registry::Rule; use crate::rules::flake8_type_checking::helpers::quote_type_expression; use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; /// ## What it does /// Checks if [PEP 613] explicit type aliases contain references to @@ -295,21 +295,20 @@ pub(crate) fn quoted_type_alias( let range = annotation_expr.range(); let mut diagnostic = checker.report_diagnostic(QuotedTypeAlias, range); let fix_string = annotation_expr.value.to_string(); + let fix_string = if (fix_string.contains('\n') || fix_string.contains('\r')) && parenthesized_range( - // Check for parenthesis outside string ("""...""") + // Check for parentheses outside the string ("""...""") annotation_expr.into(), checker.semantic().current_statement().into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.source_tokens(), ) .is_none() && parenthesized_range( - // Check for parenthesis inside string """(...)""" + // Check for parentheses inside the string """(...)""" expr.into(), annotation_expr.into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), ) .is_none() { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs index bf5a4ed8b7..840befca76 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs @@ -1,10 +1,9 @@ use std::ops::Range; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{Expr, ExprBinOp, ExprCall, Operator}; use ruff_python_semantic::SemanticModel; -use ruff_python_trivia::CommentRanges; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -89,11 +88,7 @@ pub(crate) fn path_constructor_current_directory( let mut diagnostic = checker.report_diagnostic(PathConstructorCurrentDirectory, arg.range()); - match parent_and_next_path_fragment_range( - checker.semantic(), - checker.comment_ranges(), - checker.source(), - ) { + match parent_and_next_path_fragment_range(checker.semantic(), checker.tokens()) { Some((parent_range, next_fragment_range)) => { let next_fragment_expr = checker.locator().slice(next_fragment_range); let call_expr = checker.locator().slice(call.range()); @@ -116,7 +111,7 @@ pub(crate) fn path_constructor_current_directory( arguments, Parentheses::Preserve, checker.source(), - checker.comment_ranges(), + checker.tokens(), )?; Ok(Fix::applicable_edit(edit, applicability(call.range()))) }), @@ -125,8 +120,7 @@ pub(crate) fn path_constructor_current_directory( fn parent_and_next_path_fragment_range( semantic: &SemanticModel, - comment_ranges: &CommentRanges, - source: &str, + tokens: &ruff_python_ast::token::Tokens, ) -> Option<(TextRange, TextRange)> { let parent = semantic.current_expression_parent()?; @@ -142,6 +136,6 @@ fn parent_and_next_path_fragment_range( Some(( parent.range(), - parenthesized_range(right.into(), parent.into(), comment_ranges, source).unwrap_or(range), + parenthesized_range(right.into(), parent.into(), tokens).unwrap_or(range), )) } diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs index 0fa448f7c5..71e0d82db9 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs @@ -1,8 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_const_true; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::{Tokens, parenthesized_range}; use ruff_python_ast::{self as ast, Keyword, Stmt}; -use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; use crate::Locator; @@ -91,7 +90,7 @@ pub(crate) fn inplace_argument(checker: &Checker, call: &ast::ExprCall) { call, keyword, statement, - checker.comment_ranges(), + checker.tokens(), checker.locator(), ) { diagnostic.set_fix(fix); @@ -111,21 +110,16 @@ fn convert_inplace_argument_to_assignment( call: &ast::ExprCall, keyword: &Keyword, statement: &Stmt, - comment_ranges: &CommentRanges, + tokens: &Tokens, locator: &Locator, ) -> Option { // Add the assignment. let attr = call.func.as_attribute_expr()?; let insert_assignment = Edit::insertion( format!("{name} = ", name = locator.slice(attr.value.range())), - parenthesized_range( - call.into(), - statement.into(), - comment_ranges, - locator.contents(), - ) - .unwrap_or(call.range()) - .start(), + parenthesized_range(call.into(), statement.into(), tokens) + .unwrap_or(call.range()) + .start(), ); // Remove the `inplace` argument. @@ -134,7 +128,7 @@ fn convert_inplace_argument_to_assignment( &call.arguments, Parentheses::Preserve, locator.contents(), - comment_ranges, + tokens, ) .ok()?; diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs index a42473386b..9b437aa279 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -1,5 +1,5 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{ self as ast, Expr, ExprEllipsisLiteral, ExprLambda, Identifier, Parameter, ParameterWithDefault, Parameters, Stmt, @@ -265,29 +265,19 @@ fn replace_trailing_ellipsis_with_original_expr( stmt: &Stmt, checker: &Checker, ) -> String { - let original_expr_range = parenthesized_range( - (&lambda.body).into(), - lambda.into(), - checker.comment_ranges(), - checker.source(), - ) - .unwrap_or(lambda.body.range()); + let original_expr_range = + parenthesized_range((&lambda.body).into(), lambda.into(), checker.tokens()) + .unwrap_or(lambda.body.range()); // This prevents the autofix of introducing a syntax error if the lambda's body is an // expression spanned across multiple lines. To avoid the syntax error we preserve // the parenthesis around the body. - let original_expr_in_source = if parenthesized_range( - lambda.into(), - stmt.into(), - checker.comment_ranges(), - checker.source(), - ) - .is_some() - { - format!("({})", checker.locator().slice(original_expr_range)) - } else { - checker.locator().slice(original_expr_range).to_string() - }; + let original_expr_in_source = + if parenthesized_range(lambda.into(), stmt.into(), checker.tokens()).is_some() { + format!("({})", checker.locator().slice(original_expr_range)) + } else { + checker.locator().slice(original_expr_range).to_string() + }; let placeholder_ellipsis_start = generated.rfind("...").unwrap(); let placeholder_ellipsis_end = placeholder_ellipsis_start + "...".len(); diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs index a68e492846..5ae6fe9028 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs @@ -1,4 +1,4 @@ -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::{Tokens, parenthesized_range}; use rustc_hash::FxHashMap; use ruff_macros::{ViolationMetadata, derive_message_formats}; @@ -179,15 +179,14 @@ fn is_redundant_boolean_comparison(op: CmpOp, comparator: &Expr) -> Option fn generate_redundant_comparison( compare: &ast::ExprCompare, - comment_ranges: &ruff_python_trivia::CommentRanges, + tokens: &Tokens, source: &str, comparator: &Expr, kind: bool, needs_wrap: bool, ) -> String { - let comparator_range = - parenthesized_range(comparator.into(), compare.into(), comment_ranges, source) - .unwrap_or(comparator.range()); + let comparator_range = parenthesized_range(comparator.into(), compare.into(), tokens) + .unwrap_or(comparator.range()); let comparator_str = &source[comparator_range]; @@ -379,7 +378,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) .copied() .collect::>(); - let comment_ranges = checker.comment_ranges(); + let tokens = checker.tokens(); let source = checker.source(); let content = match (&*compare.ops, &*compare.comparators) { @@ -387,18 +386,13 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) if let Some(kind) = is_redundant_boolean_comparison(*op, &compare.left) { let needs_wrap = compare.left.range().start() != compare.range().start(); generate_redundant_comparison( - compare, - comment_ranges, - source, - comparator, - kind, - needs_wrap, + compare, tokens, source, comparator, kind, needs_wrap, ) } else if let Some(kind) = is_redundant_boolean_comparison(*op, comparator) { let needs_wrap = comparator.range().end() != compare.range().end(); generate_redundant_comparison( compare, - comment_ranges, + tokens, source, &compare.left, kind, @@ -410,7 +404,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) &ops, &compare.comparators, compare.into(), - comment_ranges, + tokens, source, ) } @@ -420,7 +414,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) &ops, &compare.comparators, compare.into(), - comment_ranges, + tokens, source, ), }; diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs index a4234093bc..0c759f9e0e 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs @@ -107,7 +107,7 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) { &[CmpOp::NotIn], comparators, unary_op.into(), - checker.comment_ranges(), + checker.tokens(), checker.source(), ), unary_op.range(), @@ -127,7 +127,7 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) { &[CmpOp::IsNot], comparators, unary_op.into(), - checker.comment_ranges(), + checker.tokens(), checker.source(), ), unary_op.range(), diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs index 1acdc90138..de02e4c85a 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs @@ -3,7 +3,7 @@ use std::collections::hash_map::Entry; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::{ComparableExpr, HashableExpr}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; @@ -193,16 +193,14 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) { parenthesized_range( dict.value(i - 1).into(), dict.into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), ) .unwrap_or_else(|| dict.value(i - 1).range()) .end(), parenthesized_range( dict.value(i).into(), dict.into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), ) .unwrap_or_else(|| dict.value(i).range()) .end(), @@ -224,16 +222,14 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) { parenthesized_range( dict.value(i - 1).into(), dict.into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), ) .unwrap_or_else(|| dict.value(i - 1).range()) .end(), parenthesized_range( dict.value(i).into(), dict.into(), - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), ) .unwrap_or_else(|| dict.value(i).range()) .end(), diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs index 810c5742b9..59dcbf5c22 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::contains_effect; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::token::{TokenKind, Tokens}; use ruff_python_ast::{self as ast, Stmt}; use ruff_python_semantic::Binding; @@ -172,14 +172,10 @@ fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option { { // If the expression is complex (`x = foo()`), remove the assignment, // but preserve the right-hand side. - let start = parenthesized_range( - target.into(), - statement.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(target.range()) - .start(); + let start = + parenthesized_range(target.into(), statement.into(), checker.tokens()) + .unwrap_or(target.range()) + .start(); let end = match_token_after(checker.tokens(), target.end(), |token| { token == TokenKind::Equal })? diff --git a/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs index 27d6d49ad5..9673524a93 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{ BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare, - parenthesize::{parentheses_iterator, parenthesized_range}, + token::{parentheses_iterator, parenthesized_range}, }; use ruff_text_size::{Ranged, TextRange}; @@ -62,7 +62,7 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB } let locator = checker.locator(); - let comment_ranges = checker.comment_ranges(); + let tokens = checker.tokens(); // retrieve all compare expressions from boolean expression let compare_expressions = expr_bool_op @@ -89,40 +89,22 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB continue; } - let left_paren_count = parentheses_iterator( - left_compare.into(), - Some(expr_bool_op.into()), - comment_ranges, - locator.contents(), - ) - .count(); + let left_paren_count = + parentheses_iterator(left_compare.into(), Some(expr_bool_op.into()), tokens).count(); - let right_paren_count = parentheses_iterator( - right_compare.into(), - Some(expr_bool_op.into()), - comment_ranges, - locator.contents(), - ) - .count(); + let right_paren_count = + parentheses_iterator(right_compare.into(), Some(expr_bool_op.into()), tokens).count(); // Create the edit that removes the comparison operator // In `a<(b) and ((b)) "rsplit", }; - let maxsplit_argument_edit = fix::edits::add_argument( - "maxsplit=1", - arguments, - checker.comment_ranges(), - checker.locator().contents(), - ); + let maxsplit_argument_edit = + fix::edits::add_argument("maxsplit=1", arguments, checker.tokens()); // Only change `actual_split_type` if it doesn't match `suggested_split_type` let split_type_edit: Option = if actual_split_type == suggested_split_type { diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs b/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs index 7423c2dc76..cbe51cdd8d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs @@ -2,7 +2,7 @@ use ast::Expr; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{ExprBinOp, ExprRef, Operator}; use ruff_text_size::{Ranged, TextRange}; @@ -150,12 +150,10 @@ fn augmented_assignment( let right_operand_ref = ExprRef::from(right_operand); let parent = original_expr.into(); - let comment_ranges = checker.comment_ranges(); - let source = checker.source(); + let tokens = checker.tokens(); let right_operand_range = - parenthesized_range(right_operand_ref, parent, comment_ranges, source) - .unwrap_or(right_operand.range()); + parenthesized_range(right_operand_ref, parent, tokens).unwrap_or(right_operand.range()); let right_operand_expr = locator.slice(right_operand_range); let target_expr = locator.slice(target); diff --git a/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs b/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs index 0ed569eef8..21a4643f39 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs @@ -75,12 +75,7 @@ pub(crate) fn subprocess_run_without_check(checker: &Checker, call: &ast::ExprCa let mut diagnostic = checker.report_diagnostic(SubprocessRunWithoutCheck, call.func.range()); diagnostic.set_fix(Fix::applicable_edit( - add_argument( - "check=False", - &call.arguments, - checker.comment_ranges(), - checker.locator().contents(), - ), + add_argument("check=False", &call.arguments, checker.tokens()), // If the function call contains `**kwargs`, mark the fix as unsafe. if call .arguments diff --git a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs index 7d9cd12506..316d2e7f54 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs @@ -1,8 +1,7 @@ use std::fmt::{Display, Formatter}; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::name::QualifiedName; -use ruff_python_ast::{self as ast, Expr}; +use ruff_python_ast::{self as ast, Expr, name::QualifiedName}; use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::typing; use ruff_text_size::{Ranged, TextRange}; @@ -193,8 +192,7 @@ fn generate_keyword_fix(checker: &Checker, call: &ast::ExprCall) -> Fix { })) ), &call.arguments, - checker.comment_ranges(), - checker.locator().contents(), + checker.tokens(), )) } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs index b0d273b7c4..02f8cdd1e8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs @@ -204,7 +204,7 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD arguments, Parentheses::Remove, checker.source(), - checker.comment_ranges(), + checker.tokens(), )?; Ok(Fix::unsafe_edits( Edit::insertion(type_params.to_string(), name.end()), diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs index 43e3ec8536..6b10c3bc07 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::Name; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{Expr, ExprCall, ExprName, Keyword, StmtAnnAssign, StmtAssign, StmtRef}; use ruff_text_size::{Ranged, TextRange}; @@ -261,11 +261,11 @@ fn create_diagnostic( type_alias_kind: TypeAliasKind, ) { let source = checker.source(); + let tokens = checker.tokens(); let comment_ranges = checker.comment_ranges(); let range_with_parentheses = - parenthesized_range(value.into(), stmt.into(), comment_ranges, source) - .unwrap_or(value.range()); + parenthesized_range(value.into(), stmt.into(), tokens).unwrap_or(value.range()); let content = format!( "type {name}{type_params} = {value}", diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs index 7c5dd2f027..6ce403647d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -1,9 +1,8 @@ use anyhow::Result; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::{self as ast, Keyword}; +use ruff_python_ast::{self as ast, Keyword, token::Tokens}; use ruff_python_semantic::Modules; -use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -104,7 +103,7 @@ pub(crate) fn replace_stdout_stderr(checker: &Checker, call: &ast::ExprCall) { stderr, call, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) }); } @@ -117,7 +116,7 @@ fn generate_fix( stderr: &Keyword, call: &ast::ExprCall, source: &str, - comment_ranges: &CommentRanges, + tokens: &Tokens, ) -> Result { let (first, second) = if stdout.start() < stderr.start() { (stdout, stderr) @@ -132,7 +131,7 @@ fn generate_fix( &call.arguments, Parentheses::Preserve, source, - comment_ranges, + tokens, )?], )) } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs index 97000fa75f..b7b13c7b66 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs @@ -78,7 +78,7 @@ pub(crate) fn replace_universal_newlines(checker: &Checker, call: &ast::ExprCall &call.arguments, Parentheses::Preserve, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) .map(Fix::safe_edit) }); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs index 01cfe4fb03..792365042f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs @@ -188,7 +188,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) .map(Fix::safe_edit) }); @@ -206,7 +206,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) .map(Fix::safe_edit) }); @@ -231,7 +231,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) .map(Fix::safe_edit) }); @@ -249,7 +249,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) .map(Fix::safe_edit) }); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs index 75bbe20eca..20d3f64461 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs @@ -70,7 +70,7 @@ pub(crate) fn useless_class_metaclass_type(checker: &Checker, class_def: &StmtCl arguments, Parentheses::Remove, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), )?; let range = edit.range(); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs index 4a5789c78f..a1b0d900f8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -73,7 +73,7 @@ pub(crate) fn useless_object_inheritance(checker: &Checker, class_def: &ast::Stm arguments, Parentheses::Remove, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), )?; let range = edit.range(); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs index 7fec2b7d79..55f2afc89a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs @@ -1,5 +1,5 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_text_size::Ranged; @@ -139,13 +139,8 @@ pub(crate) fn yield_in_for_loop(checker: &Checker, stmt_for: &ast::StmtFor) { let mut diagnostic = checker.report_diagnostic(YieldInForLoop, stmt_for.range()); let contents = checker.locator().slice( - parenthesized_range( - iter.as_ref().into(), - stmt_for.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(iter.range()), + parenthesized_range(iter.as_ref().into(), stmt_for.into(), checker.tokens()) + .unwrap_or(iter.range()), ); let contents = if iter.as_tuple_expr().is_some_and(|it| !it.parenthesized) { format!("yield from ({contents})") diff --git a/crates/ruff_linter/src/rules/refurb/helpers.rs b/crates/ruff_linter/src/rules/refurb/helpers.rs index 0a09d70aba..a6871f0497 100644 --- a/crates/ruff_linter/src/rules/refurb/helpers.rs +++ b/crates/ruff_linter/src/rules/refurb/helpers.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use ruff_python_ast::PythonVersion; -use ruff_python_ast::{self as ast, Expr, name::Name, parenthesize::parenthesized_range}; +use ruff_python_ast::{self as ast, Expr, name::Name, token::parenthesized_range}; use ruff_python_codegen::Generator; use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel}; use ruff_text_size::{Ranged, TextRange}; @@ -330,12 +330,8 @@ pub(super) fn parenthesize_loop_iter_if_necessary<'a>( let locator = checker.locator(); let iter = for_stmt.iter.as_ref(); - let original_parenthesized_range = parenthesized_range( - iter.into(), - for_stmt.into(), - checker.comment_ranges(), - checker.source(), - ); + let original_parenthesized_range = + parenthesized_range(iter.into(), for_stmt.into(), checker.tokens()); if let Some(range) = original_parenthesized_range { return Cow::Borrowed(locator.slice(range)); diff --git a/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs b/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs index 3622148fbf..b2b3193d9e 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs @@ -1,5 +1,5 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{ Expr, ExprAttribute, ExprBinOp, ExprCall, ExprStringLiteral, ExprSubscript, ExprUnaryOp, Number, Operator, PythonVersion, UnaryOp, @@ -112,8 +112,7 @@ pub(crate) fn fromisoformat_replace_z(checker: &Checker, call: &ExprCall) { let value_full_range = parenthesized_range( replace_time_zone.date.into(), replace_time_zone.parent.into(), - checker.comment_ranges(), - checker.source(), + checker.tokens(), ) .unwrap_or(replace_time_zone.date.range()); diff --git a/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs b/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs index fa660587ef..f4f4d1f7b7 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs @@ -5,8 +5,7 @@ use ruff_python_ast as ast; use ruff_python_ast::Expr; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::contains_effect; -use ruff_python_ast::parenthesize::parenthesized_range; -use ruff_python_trivia::CommentRanges; +use ruff_python_ast::token::{Tokens, parenthesized_range}; use ruff_text_size::Ranged; use crate::Locator; @@ -76,8 +75,8 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &Checker, if_expr: &ast::Ex Edit::range_replacement( format!( "{} or {}", - parenthesize_test(test, if_expr, checker.comment_ranges(), checker.locator()), - parenthesize_test(orelse, if_expr, checker.comment_ranges(), checker.locator()), + parenthesize_test(test, if_expr, checker.tokens(), checker.locator()), + parenthesize_test(orelse, if_expr, checker.tokens(), checker.locator()), ), if_expr.range(), ), @@ -99,15 +98,10 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &Checker, if_expr: &ast::Ex fn parenthesize_test<'a>( expr: &Expr, if_expr: &ast::ExprIf, - comment_ranges: &CommentRanges, + tokens: &Tokens, locator: &Locator<'a>, ) -> Cow<'a, str> { - if let Some(range) = parenthesized_range( - expr.into(), - if_expr.into(), - comment_ranges, - locator.contents(), - ) { + if let Some(range) = parenthesized_range(expr.into(), if_expr.into(), tokens) { Cow::Borrowed(locator.slice(range)) } else if matches!(expr, Expr::If(_) | Expr::Lambda(_) | Expr::Named(_)) { Cow::Owned(format!("({})", locator.slice(expr.range()))) diff --git a/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs b/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs index a6ea1eb570..943a013cbb 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::Applicability; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{Comprehension, Expr, StmtFor}; use ruff_python_semantic::analyze::typing; use ruff_python_semantic::analyze::typing::is_io_base_expr; @@ -104,8 +104,7 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) { let deletion_range = if let Some(parenthesized_range) = parenthesized_range( expr_attr.value.as_ref().into(), expr_attr.into(), - checker.comment_ranges(), - checker.source(), + checker.tokens(), ) { expr_call.range().add_start(parenthesized_range.len()) } else { diff --git a/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs b/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs index a54bd261d1..35774cde28 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs @@ -1,7 +1,7 @@ use anyhow::Result; use ruff_diagnostics::Applicability; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr, Number}; use ruff_text_size::Ranged; @@ -152,13 +152,8 @@ fn generate_fix(checker: &Checker, call: &ast::ExprCall, base: Base, arg: &Expr) checker.semantic(), )?; - let arg_range = parenthesized_range( - arg.into(), - call.into(), - checker.comment_ranges(), - checker.source(), - ) - .unwrap_or(arg.range()); + let arg_range = + parenthesized_range(arg.into(), call.into(), checker.tokens()).unwrap_or(arg.range()); let arg_str = checker.locator().slice(arg_range); Ok(Fix::applicable_edits( diff --git a/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs b/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs index 1df2fdde78..04b00f07f7 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs @@ -95,7 +95,7 @@ pub(crate) fn single_item_membership_test( &[membership_test.replacement_op()], std::slice::from_ref(item), expr.into(), - checker.comment_ranges(), + checker.tokens(), checker.source(), ), expr.range(), diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs b/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs index 4178217718..7b023de830 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs @@ -163,7 +163,7 @@ fn convert_type_vars( class_arguments, Parentheses::Remove, source, - checker.comment_ranges(), + checker.tokens(), )?; let replace_type_params = Edit::range_replacement(new_type_params.to_string(), type_params.range); diff --git a/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs b/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs index ea792ce97c..70ef8cb6d4 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs @@ -3,8 +3,8 @@ use anyhow::Result; use ast::Keyword; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_constant; +use ruff_python_ast::token::Tokens; use ruff_python_ast::{self as ast, Expr}; -use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; use crate::Locator; @@ -108,9 +108,8 @@ pub(crate) fn default_factory_kwarg(checker: &Checker, call: &ast::ExprCall) { }, call.range(), ); - diagnostic.try_set_fix(|| { - convert_to_positional(call, keyword, checker.locator(), checker.comment_ranges()) - }); + diagnostic + .try_set_fix(|| convert_to_positional(call, keyword, checker.locator(), checker.tokens())); } /// Returns `true` if a value is definitively not callable (e.g., `1` or `[]`). @@ -136,7 +135,7 @@ fn convert_to_positional( call: &ast::ExprCall, default_factory: &Keyword, locator: &Locator, - comment_ranges: &CommentRanges, + tokens: &Tokens, ) -> Result { if call.arguments.len() == 1 { // Ex) `defaultdict(default_factory=list)` @@ -153,7 +152,7 @@ fn convert_to_positional( &call.arguments, Parentheses::Preserve, locator.contents(), - comment_ranges, + tokens, )?; // Second, insert the value as the first positional argument. diff --git a/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs b/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs index a19a9c451a..de3c072bdd 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs @@ -128,7 +128,7 @@ pub(crate) fn falsy_dict_get_fallback(checker: &Checker, expr: &Expr) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), - checker.comment_ranges(), + checker.tokens(), ) .map(|edit| Fix::applicable_edit(edit, applicability)) }); diff --git a/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs b/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs index 7433f63f2b..6e2351aafb 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs @@ -1,6 +1,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -77,14 +77,7 @@ pub(crate) fn parenthesize_chained_logical_operators(checker: &Checker, expr: &a ) => { let locator = checker.locator(); let source_range = bool_op.range(); - if parenthesized_range( - bool_op.into(), - expr.into(), - checker.comment_ranges(), - locator.contents(), - ) - .is_none() - { + if parenthesized_range(bool_op.into(), expr.into(), checker.tokens()).is_none() { let new_source = format!("({})", locator.slice(source_range)); let edit = Edit::range_replacement(new_source, source_range); checker diff --git a/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs b/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs index 36941a98c9..2ec2472262 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs @@ -2,7 +2,7 @@ use anyhow::Context; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_semantic::{Scope, ScopeKind}; use ruff_python_trivia::{indentation_at_offset, textwrap}; use ruff_source_file::LineRanges; @@ -159,8 +159,7 @@ fn use_initvar( let default_loc = parenthesized_range( default.into(), parameter_with_default.into(), - checker.comment_ranges(), - checker.source(), + checker.tokens(), ) .unwrap_or(default.range()); diff --git a/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs b/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs index d4e062ad71..a38fd2fd6d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs @@ -2,7 +2,7 @@ use anyhow::Result; use itertools::Itertools; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Arguments, Expr}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; @@ -116,13 +116,8 @@ fn convert_to_reduce(iterable: &Expr, call: &ast::ExprCall, checker: &Checker) - )?; let iterable = checker.locator().slice( - parenthesized_range( - iterable.into(), - (&call.arguments).into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(iterable.range()), + parenthesized_range(iterable.into(), (&call.arguments).into(), checker.tokens()) + .unwrap_or(iterable.range()), ); Ok(Fix::unsafe_edits( diff --git a/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs b/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs index e9ed8d31bb..79881b8abc 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs @@ -1,7 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::PythonVersion; use ruff_python_ast::token::TokenKind; -use ruff_python_ast::{Expr, ExprCall, parenthesize::parenthesized_range}; +use ruff_python_ast::{Expr, ExprCall, token::parenthesized_range}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -124,13 +124,8 @@ fn replace_with_map(starmap: &ExprCall, zip: &ExprCall, checker: &Checker) -> Op let mut remove_zip = vec![]; - let full_zip_range = parenthesized_range( - zip.into(), - starmap.into(), - checker.comment_ranges(), - checker.source(), - ) - .unwrap_or(zip.range()); + let full_zip_range = + parenthesized_range(zip.into(), starmap.into(), checker.tokens()).unwrap_or(zip.range()); // Delete any parentheses around the `zip` call to prevent that the argument turns into a tuple. remove_zip.push(Edit::range_deletion(TextRange::new( @@ -138,13 +133,8 @@ fn replace_with_map(starmap: &ExprCall, zip: &ExprCall, checker: &Checker) -> Op zip.start(), ))); - let full_zip_func_range = parenthesized_range( - (&zip.func).into(), - zip.into(), - checker.comment_ranges(), - checker.source(), - ) - .unwrap_or(zip.func.range()); + let full_zip_func_range = parenthesized_range((&zip.func).into(), zip.into(), checker.tokens()) + .unwrap_or(zip.func.range()); // Delete the `zip` callee remove_zip.push(Edit::range_deletion(full_zip_func_range)); diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index b3c34c29e0..453aa07801 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -1,5 +1,5 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::{Tokens, parenthesized_range}; use ruff_python_ast::{Arguments, Expr, ExprCall}; use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; @@ -86,6 +86,7 @@ pub(crate) fn unnecessary_cast_to_int(checker: &Checker, call: &ExprCall) { applicability, checker.semantic(), checker.locator(), + checker.tokens(), checker.comment_ranges(), checker.source(), ); @@ -95,27 +96,26 @@ pub(crate) fn unnecessary_cast_to_int(checker: &Checker, call: &ExprCall) { } /// Creates a fix that replaces `int(expression)` with `expression`. +#[allow(clippy::too_many_arguments)] fn unwrap_int_expression( call: &ExprCall, argument: &Expr, applicability: Applicability, semantic: &SemanticModel, locator: &Locator, + tokens: &Tokens, comment_ranges: &CommentRanges, source: &str, ) -> Fix { - let content = if let Some(range) = parenthesized_range( - argument.into(), - (&call.arguments).into(), - comment_ranges, - source, - ) { + let content = if let Some(range) = + parenthesized_range(argument.into(), (&call.arguments).into(), tokens) + { locator.slice(range).to_string() } else { let parenthesize = semantic.current_expression_parent().is_some() || argument.is_named_expr() || locator.count_lines(argument.range()) > 0; - if parenthesize && !has_own_parentheses(argument, comment_ranges, source) { + if parenthesize && !has_own_parentheses(argument, tokens, source) { format!("({})", locator.slice(argument.range())) } else { locator.slice(argument.range()).to_string() @@ -255,7 +255,7 @@ fn round_applicability(arguments: &Arguments, semantic: &SemanticModel) -> Optio } /// Returns `true` if the given [`Expr`] has its own parentheses (e.g., `()`, `[]`, `{}`). -fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str) -> bool { +fn has_own_parentheses(expr: &Expr, tokens: &Tokens, source: &str) -> bool { match expr { Expr::ListComp(_) | Expr::SetComp(_) @@ -276,14 +276,10 @@ fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str // f // (10) // ``` - let func_end = parenthesized_range( - call_expr.func.as_ref().into(), - call_expr.into(), - comment_ranges, - source, - ) - .unwrap_or(call_expr.func.range()) - .end(); + let func_end = + parenthesized_range(call_expr.func.as_ref().into(), call_expr.into(), tokens) + .unwrap_or(call_expr.func.range()) + .end(); lines_after_ignoring_trivia(func_end, source) == 0 } Expr::Subscript(subscript_expr) => { @@ -291,8 +287,7 @@ fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str let subscript_end = parenthesized_range( subscript_expr.value.as_ref().into(), subscript_expr.into(), - comment_ranges, - source, + tokens, ) .unwrap_or(subscript_expr.value.range()) .end(); diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs index 7c13fb3d1c..502391dcf2 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs @@ -3,7 +3,7 @@ use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::contains_effect; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -108,22 +108,12 @@ pub(crate) fn unnecessary_key_check(checker: &Checker, expr: &Expr) { format!( "{}.get({})", checker.locator().slice( - parenthesized_range( - obj_right.into(), - right.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(obj_right.range()) + parenthesized_range(obj_right.into(), right.into(), checker.tokens(),) + .unwrap_or(obj_right.range()) ), checker.locator().slice( - parenthesized_range( - key_right.into(), - right.into(), - checker.comment_ranges(), - checker.locator().contents(), - ) - .unwrap_or(key_right.range()) + parenthesized_range(key_right.into(), right.into(), checker.tokens(),) + .unwrap_or(key_right.range()) ), ), expr.range(), diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs index fd31765715..b322d57fd6 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs @@ -2,7 +2,7 @@ use ruff_diagnostics::{Applicability, Edit}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_empty_f_string; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; @@ -140,31 +140,19 @@ fn fix_unnecessary_literal_in_deque( // call. otherwise, we only delete the `iterable` argument and leave the others untouched. let edit = if let Some(maxlen) = maxlen { let deque_name = checker.locator().slice( - parenthesized_range( - deque.func.as_ref().into(), - deque.into(), - checker.comment_ranges(), - checker.source(), - ) - .unwrap_or(deque.func.range()), + parenthesized_range(deque.func.as_ref().into(), deque.into(), checker.tokens()) + .unwrap_or(deque.func.range()), ); let len_str = checker.locator().slice(maxlen); let deque_str = format!("{deque_name}(maxlen={len_str})"); Edit::range_replacement(deque_str, deque.range) } else { - let range = parenthesized_range( - iterable.value().into(), - (&deque.arguments).into(), - checker.comment_ranges(), - checker.source(), - ) - .unwrap_or(iterable.range()); remove_argument( - &range, + &iterable, &deque.arguments, Parentheses::Preserve, checker.source(), - checker.comment_ranges(), + checker.tokens(), )? }; let has_comments = checker.comment_ranges().intersects(edit.range()); diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 4879e04780..9680d03ec3 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -3,13 +3,14 @@ use std::path::Path; use rustc_hash::FxHashMap; -use ruff_python_trivia::{CommentRanges, SimpleTokenKind, SimpleTokenizer, indentation_at_offset}; +use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer, indentation_at_offset}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::name::{Name, QualifiedName, QualifiedNameBuilder}; -use crate::parenthesize::parenthesized_range; use crate::statement_visitor::StatementVisitor; +use crate::token::Tokens; +use crate::token::parenthesized_range; use crate::visitor::Visitor; use crate::{ self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr, ExprNoneLiteral, @@ -1474,7 +1475,7 @@ pub fn generate_comparison( ops: &[CmpOp], comparators: &[Expr], parent: AnyNodeRef, - comment_ranges: &CommentRanges, + tokens: &Tokens, source: &str, ) -> String { let start = left.start(); @@ -1483,8 +1484,7 @@ pub fn generate_comparison( // Add the left side of the comparison. contents.push_str( - &source[parenthesized_range(left.into(), parent, comment_ranges, source) - .unwrap_or(left.range())], + &source[parenthesized_range(left.into(), parent, tokens).unwrap_or(left.range())], ); for (op, comparator) in ops.iter().zip(comparators) { @@ -1504,7 +1504,7 @@ pub fn generate_comparison( // Add the right side of the comparison. contents.push_str( - &source[parenthesized_range(comparator.into(), parent, comment_ranges, source) + &source[parenthesized_range(comparator.into(), parent, tokens) .unwrap_or(comparator.range())], ); } diff --git a/crates/ruff_python_formatter/src/comments/debug.rs b/crates/ruff_python_formatter/src/comments/debug.rs index 3e4ae7c6bd..0adbba24f3 100644 --- a/crates/ruff_python_formatter/src/comments/debug.rs +++ b/crates/ruff_python_formatter/src/comments/debug.rs @@ -36,7 +36,7 @@ impl Debug for DebugComment<'_> { } } -/// Pretty-printed debug representation of [`Comments`]. +/// Pretty-printed debug representation of [`Comments`](super::Comments). pub(crate) struct DebugComments<'a> { comments: &'a CommentsMap<'a>, source_code: SourceCode<'a>, diff --git a/crates/ruff_python_formatter/src/comments/map.rs b/crates/ruff_python_formatter/src/comments/map.rs index 3f6d621a59..8d25f4f8a7 100644 --- a/crates/ruff_python_formatter/src/comments/map.rs +++ b/crates/ruff_python_formatter/src/comments/map.rs @@ -504,7 +504,7 @@ impl InOrderEntry { #[derive(Clone, Debug)] struct OutOfOrderEntry { - /// Index into the [`MultiMap::out_of_order`] vector at which offset the leading vec is stored. + /// Index into the [`MultiMap::out_of_order_parts`] vector at which offset the leading vec is stored. leading_index: usize, _count: Count, } diff --git a/crates/ruff_python_formatter/src/comments/node_key.rs b/crates/ruff_python_formatter/src/comments/node_key.rs index ec15ced488..115a751150 100644 --- a/crates/ruff_python_formatter/src/comments/node_key.rs +++ b/crates/ruff_python_formatter/src/comments/node_key.rs @@ -2,7 +2,8 @@ use ruff_python_ast::AnyNodeRef; use std::fmt::{Debug, Formatter}; use std::hash::{Hash, Hasher}; -/// Used as key into the [`MultiMap`] storing the comments per node by [`Comments`]. +/// Used as key into the [`MultiMap`](super::MultiMap) storing the comments per node by +/// [`Comments`](super::Comments). /// /// Implements equality and hashing based on the address of the [`AnyNodeRef`] to get fast and cheap /// hashing/equality comparison. diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 76449285be..ff38d7a100 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1974,8 +1974,8 @@ fn handle_unary_op_comment<'a>( /// ) /// ``` /// -/// The comment will be attached to the [`Arguments`] node as a dangling comment, to ensure -/// that it remains on the same line as open parenthesis. +/// The comment will be attached to the [`Arguments`](ast::Arguments) node as a dangling comment, to +/// ensure that it remains on the same line as open parenthesis. /// /// Similarly, given: /// ```python @@ -1984,8 +1984,8 @@ fn handle_unary_op_comment<'a>( /// ] = ... /// ``` /// -/// The comment will be attached to the [`TypeParams`] node as a dangling comment, to ensure -/// that it remains on the same line as open bracket. +/// The comment will be attached to the [`TypeParams`](ast::TypeParams) node as a dangling comment, +/// to ensure that it remains on the same line as open bracket. fn handle_bracketed_end_of_line_comment<'a>( comment: DecoratedComment<'a>, source: &str, diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index b9670a3ee8..1aad7e273d 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -174,7 +174,8 @@ impl<'ast> SourceOrderVisitor<'ast> for CommentsVisitor<'ast, '_> { /// A comment decorated with additional information about its surrounding context in the source document. /// -/// Used by [`CommentStyle::place_comment`] to determine if this should become a [leading](self#leading-comments), [dangling](self#dangling-comments), or [trailing](self#trailing-comments) comment. +/// Used by [`place_comment`] to determine if this should become a [leading](self#leading-comments), +/// [dangling](self#dangling-comments), or [trailing](self#trailing-comments) comment. #[derive(Debug, Clone)] pub(crate) struct DecoratedComment<'a> { enclosing: AnyNodeRef<'a>, @@ -465,7 +466,7 @@ pub(super) enum CommentPlacement<'a> { /// /// [`preceding_node`]: DecoratedComment::preceding_node /// [`following_node`]: DecoratedComment::following_node - /// [`enclosing_node`]: DecoratedComment::enclosing_node_id + /// [`enclosing_node`]: DecoratedComment::enclosing_node /// [trailing comment]: self#trailing-comments /// [leading comment]: self#leading-comments /// [dangling comment]: self#dangling-comments diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 239edc8d5b..8eaf52ee35 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -166,7 +166,7 @@ impl InterpolatedStringState { } } - /// Returns `true` if the interpolated string state is [`NestedInterpolatedElement`]. + /// Returns `true` if the interpolated string state is [`Self::NestedInterpolatedElement`]. pub(crate) fn is_nested(self) -> bool { matches!(self, Self::NestedInterpolatedElement(..)) } diff --git a/crates/ruff_python_formatter/src/expression/binary_like.rs b/crates/ruff_python_formatter/src/expression/binary_like.rs index 5d86608452..f1da88299d 100644 --- a/crates/ruff_python_formatter/src/expression/binary_like.rs +++ b/crates/ruff_python_formatter/src/expression/binary_like.rs @@ -1095,9 +1095,9 @@ impl OperandIndex { } } - /// Returns the index of the operand's right operator. The method always returns an index - /// even if the operand has no right operator. Use [`BinaryCallChain::get_operator`] to test if - /// the operand has a right operator. + /// Returns the index of the operand's right operator. The method always returns an index even + /// if the operand has no right operator. Use [`FlatBinaryExpressionSlice::get_operator`] to + /// test if the operand has a right operator. fn right_operator(self) -> OperatorIndex { OperatorIndex::new(self.0 + 1) } diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index a76f8a0aec..18b9b274c1 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -56,18 +56,20 @@ pub(crate) enum Parenthesize { /// Adding parentheses is desired to prevent the comments from wandering. IfRequired, - /// Same as [`Self::IfBreaks`] except that it uses [`parenthesize_if_expands`] for expressions - /// with the layout [`NeedsParentheses::BestFit`] which is used by non-splittable - /// expressions like literals, name, and strings. + /// Same as [`Self::IfBreaks`] except that it uses + /// [`parenthesize_if_expands`](crate::builders::parenthesize_if_expands) for expressions with + /// the layout [`OptionalParentheses::BestFit`] which is used by non-splittable expressions like + /// literals, name, and strings. /// /// Use this layout over `IfBreaks` when there's a sequence of `maybe_parenthesize_expression` /// in a single logical-line and you want to break from right-to-left. Use `IfBreaks` for the /// first expression and `IfBreaksParenthesized` for the rest. IfBreaksParenthesized, - /// Same as [`Self::IfBreaksParenthesized`] but uses [`parenthesize_if_expands`] for nested - /// [`maybe_parenthesized_expression`] calls unlike other layouts that always omit parentheses - /// when outer parentheses are present. + /// Same as [`Self::IfBreaksParenthesized`] but uses + /// [`parenthesize_if_expands`](crate::builders::parenthesize_if_expands) for nested + /// [`maybe_parenthesized_expression`](crate::expression::maybe_parenthesize_expression) calls + /// unlike other layouts that always omit parentheses when outer parentheses are present. IfBreaksParenthesizedNested, } diff --git a/crates/ruff_python_formatter/src/pattern/mod.rs b/crates/ruff_python_formatter/src/pattern/mod.rs index a379aeb849..ac763a41ff 100644 --- a/crates/ruff_python_formatter/src/pattern/mod.rs +++ b/crates/ruff_python_formatter/src/pattern/mod.rs @@ -214,8 +214,9 @@ impl Format> for MaybeParenthesizePattern<'_> { } } -/// This function is very similar to [`can_omit_optional_parentheses`] with the only difference that it is for patterns -/// and not expressions. +/// This function is very similar to +/// [`can_omit_optional_parentheses`](crate::expression::can_omit_optional_parentheses) +/// with the only difference that it is for patterns and not expressions. /// /// The base idea of the omit optional parentheses layout is to prefer using parentheses of sub-patterns /// when splitting the pattern over introducing new patterns. For example, prefer splitting the sequence pattern in diff --git a/crates/ruff_python_formatter/src/pattern/pattern_arguments.rs b/crates/ruff_python_formatter/src/pattern/pattern_arguments.rs index 94d7448226..d82a9756e0 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_arguments.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_arguments.rs @@ -72,8 +72,9 @@ impl FormatNodeRule for FormatPatternArguments { } } -/// Returns `true` if the pattern (which is the only argument to a [`PatternMatchClass`]) is -/// parenthesized. Used to avoid falsely assuming that `x` is parenthesized in cases like: +/// Returns `true` if the pattern (which is the only argument to a +/// [`PatternMatchClass`](ruff_python_ast::PatternMatchClass)) is parenthesized. +/// Used to avoid falsely assuming that `x` is parenthesized in cases like: /// ```python /// case Point2D(x): ... /// ``` diff --git a/crates/ruff_python_formatter/src/range.rs b/crates/ruff_python_formatter/src/range.rs index 436c1a12c2..ccc7a51aba 100644 --- a/crates/ruff_python_formatter/src/range.rs +++ b/crates/ruff_python_formatter/src/range.rs @@ -23,7 +23,8 @@ use crate::{FormatModuleError, PyFormatOptions, format_module_source}; /// /// The returned formatted range guarantees to cover at least `range` (excluding whitespace), but the range might be larger. /// Some cases in which the returned range is larger than `range` are: -/// * The logical lines in `range` use a indentation different from the configured [`IndentStyle`] and [`IndentWidth`]. +/// * The logical lines in `range` use a indentation different from the configured [`IndentStyle`] +/// and [`IndentWidth`](ruff_formatter::IndentWidth). /// * `range` is smaller than a logical lines and the formatter needs to format the entire logical line. /// * `range` falls on a single line body. /// @@ -129,16 +130,19 @@ pub fn format_range( /// b) formatting a sub-expression has fewer split points than formatting the entire expressions. /// /// ### Possible docstrings -/// Strings that are suspected to be docstrings are excluded from the search to format the enclosing suite instead -/// so that the formatter's docstring detection in [`FormatSuite`] correctly detects and formats the docstrings. +/// Strings that are suspected to be docstrings are excluded from the search to format the enclosing +/// suite instead so that the formatter's docstring detection in +/// [`FormatSuite`](crate::statement::suite::FormatSuite) correctly detects and formats the +/// docstrings. /// /// ### Compound statements with a simple statement body /// Don't include simple-statement bodies of compound statements `if True: pass` because the formatter -/// must run [`FormatClauseBody`] to determine if the body should be collapsed or not. +/// must run `FormatClauseBody` to determine if the body should be collapsed or not. /// /// ### Incorrectly indented code -/// Code that uses indentations that don't match the configured [`IndentStyle`] and [`IndentWidth`] are excluded from the search, -/// because formatting such nodes on their own can lead to indentation mismatch with its sibling nodes. +/// Code that uses indentations that don't match the configured [`IndentStyle`] and +/// [`IndentWidth`](ruff_formatter::IndentWidth) are excluded from the search, because formatting +/// such nodes on their own can lead to indentation mismatch with its sibling nodes. /// /// ## Suppression comments /// The search ends when `range` falls into a suppressed range because there's nothing to format. It also avoids that the @@ -279,13 +283,15 @@ enum EnclosingNode<'a> { /// /// ## Compound statements with simple statement bodies /// Similar to [`find_enclosing_node`], exclude the compound statement's body if it is a simple statement (not a suite) from the search to format the entire clause header -/// with the body. This ensures that the formatter runs [`FormatClauseBody`] that determines if the body should be indented.s +/// with the body. This ensures that the formatter runs `FormatClauseBody` that determines if the body should be indented. /// /// ## Non-standard indentation -/// Node's that use an indentation that doesn't match the configured [`IndentStyle`] and [`IndentWidth`] are excluded from the search. -/// This is because the formatter always uses the configured [`IndentStyle`] and [`IndentWidth`], resulting in the -/// formatted nodes using a different indentation than the unformatted sibling nodes. This would be tolerable -/// in non whitespace sensitive languages like JavaScript but results in lexical errors in Python. +/// Nodes that use an indentation that doesn't match the configured [`IndentStyle`] and +/// [`IndentWidth`](ruff_formatter::IndentWidth) are excluded from the search. This is because the +/// formatter always uses the configured [`IndentStyle`] and +/// [`IndentWidth`](ruff_formatter::IndentWidth), resulting in the formatted nodes using a different +/// indentation than the unformatted sibling nodes. This would be tolerable in non whitespace +/// sensitive languages like JavaScript but results in lexical errors in Python. /// /// ## Implementation /// It would probably be possible to merge this visitor with [`FindEnclosingNode`] but they are separate because @@ -713,9 +719,11 @@ impl Format> for FormatEnclosingNode<'_> { } } -/// Computes the level of indentation for `indentation` when using the configured [`IndentStyle`] and [`IndentWidth`]. +/// Computes the level of indentation for `indentation` when using the configured [`IndentStyle`] +/// and [`IndentWidth`](ruff_formatter::IndentWidth). /// -/// Returns `None` if the indentation doesn't conform to the configured [`IndentStyle`] and [`IndentWidth`]. +/// Returns `None` if the indentation doesn't conform to the configured [`IndentStyle`] and +/// [`IndentWidth`](ruff_formatter::IndentWidth). /// /// # Panics /// If `offset` is outside of `source`. diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index b9fbe6b7a3..5a16e5e8bf 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -184,7 +184,7 @@ impl Format> for FormatTargetWithEqualOperator<'_> { /// No parentheses are added for `short` because it fits into the configured line length, regardless of whether /// the comment exceeds the line width or not. /// -/// This logic isn't implemented in [`place_comment`] by associating trailing statement comments to the expression because +/// This logic isn't implemented in `place_comment` by associating trailing statement comments to the expression because /// doing so breaks the suite empty lines formatting that relies on trailing comments to be stored on the statement. #[derive(Debug)] pub(super) enum FormatStatementsLastExpression<'a> { @@ -202,8 +202,8 @@ pub(super) enum FormatStatementsLastExpression<'a> { /// ] = some_long_value /// ``` /// - /// This layout is preferred over [`RightToLeft`] if the left is unsplittable (single keyword like `return` or a Name) - /// because it has better performance characteristics. + /// This layout is preferred over [`Self::RightToLeft`] if the left is unsplittable (single + /// keyword like `return` or a Name) because it has better performance characteristics. LeftToRight { /// The right side of an assignment or the value returned in a return statement. value: &'a Expr, @@ -1083,11 +1083,10 @@ impl Format> for InterpolatedString<'_> { /// For legibility, we discuss only the case of f-strings below, but the /// same comments apply to t-strings. /// -/// This is just a wrapper around [`FormatFString`] while considering a special -/// case when the f-string is at an assignment statement's value position. -/// This is necessary to prevent an instability where an f-string contains a -/// multiline expression and the f-string fits on the line, but only when it's -/// surrounded by parentheses. +/// This is just a wrapper around [`FormatFString`](crate::other::f_string::FormatFString) while +/// considering a special case when the f-string is at an assignment statement's value position. +/// This is necessary to prevent an instability where an f-string contains a multiline expression +/// and the f-string fits on the line, but only when it's surrounded by parentheses. /// /// ```python /// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index 3ef8e52a23..4e2c6bc876 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -177,8 +177,10 @@ enum WithItemsLayout<'a> { /// ... /// ``` /// - /// In this case, use [`maybe_parenthesize_expression`] to format the context expression - /// to get the exact same formatting as when formatting an expression in any other clause header. + /// In this case, use + /// [`maybe_parenthesize_expression`](crate::expression::maybe_parenthesize_expression) to + /// format the context expression to get the exact same formatting as when formatting an + /// expression in any other clause header. /// /// Only used for Python 3.9+ /// diff --git a/crates/ruff_python_formatter/src/string/docstring.rs b/crates/ruff_python_formatter/src/string/docstring.rs index ad357fc65e..c4554175d5 100644 --- a/crates/ruff_python_formatter/src/string/docstring.rs +++ b/crates/ruff_python_formatter/src/string/docstring.rs @@ -783,7 +783,7 @@ enum CodeExampleKind<'src> { /// /// Documentation describing doctests and how they're recognized can be /// found as part of the Python standard library: - /// https://docs.python.org/3/library/doctest.html. + /// . /// /// (You'll likely need to read the [regex matching] used internally by the /// doctest module to determine more precisely how it works.) diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs index fabd10a029..1500737950 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -38,8 +38,9 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { /// it can't because the string contains the preferred quotes OR /// it leads to more escaping. /// - /// Note: If you add more cases here where we return `QuoteStyle::Preserve`, - /// make sure to also add them to [`FormatImplicitConcatenatedStringFlat::new`]. + /// Note: If you add more cases here where we return `QuoteStyle::Preserve`, make sure to also + /// add them to + /// [`FormatImplicitConcatenatedStringFlat::new`](crate::string::implicit::FormatImplicitConcatenatedStringFlat::new). pub(super) fn preferred_quote_style(&self, string: StringLikePart) -> QuoteStyle { let preferred_quote_style = self .preferred_quote_style diff --git a/crates/ruff_python_formatter/src/type_param/type_params.rs b/crates/ruff_python_formatter/src/type_param/type_params.rs index e243dc6006..50c2cc5770 100644 --- a/crates/ruff_python_formatter/src/type_param/type_params.rs +++ b/crates/ruff_python_formatter/src/type_param/type_params.rs @@ -9,7 +9,7 @@ use crate::prelude::*; #[derive(Default)] pub struct FormatTypeParams; -/// Formats a sequence of [`TypeParam`] nodes. +/// Formats a sequence of [`TypeParam`](ruff_python_ast::TypeParam) nodes. impl FormatNodeRule for FormatTypeParams { fn fmt_fields(&self, item: &TypeParams, f: &mut PyFormatter) -> FormatResult<()> { // A dangling comment indicates a comment on the same line as the opening bracket, e.g.: diff --git a/crates/ruff_python_formatter/src/verbatim.rs b/crates/ruff_python_formatter/src/verbatim.rs index e0bbf00ad6..3dc78a92b7 100644 --- a/crates/ruff_python_formatter/src/verbatim.rs +++ b/crates/ruff_python_formatter/src/verbatim.rs @@ -679,8 +679,9 @@ impl Indentation { /// Returns `true` for a space or tab character. /// -/// This is different than [`is_python_whitespace`] in that it returns `false` for a form feed character. -/// Form feed characters are excluded because they should be preserved in the suppressed output. +/// This is different than [`is_python_whitespace`](ruff_python_trivia::is_python_whitespace) in +/// that it returns `false` for a form feed character. Form feed characters are excluded because +/// they should be preserved in the suppressed output. const fn is_indent_whitespace(c: char) -> bool { matches!(c, ' ' | '\t') } diff --git a/crates/ruff_python_parser/Cargo.toml b/crates/ruff_python_parser/Cargo.toml index ae45871866..c527f96e11 100644 --- a/crates/ruff_python_parser/Cargo.toml +++ b/crates/ruff_python_parser/Cargo.toml @@ -35,6 +35,7 @@ ruff_source_file = { workspace = true } anyhow = { workspace = true } insta = { workspace = true, features = ["glob"] } +itertools = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } walkdir = { workspace = true } diff --git a/crates/ruff_python_parser/resources/invalid/re_lexing/ty_1828.py b/crates/ruff_python_parser/resources/invalid/re_lexing/ty_1828.py new file mode 100644 index 0000000000..3e953b541f --- /dev/null +++ b/crates/ruff_python_parser/resources/invalid/re_lexing/ty_1828.py @@ -0,0 +1,5 @@ +# Regression test for https://github.com/astral-sh/ty/issues/1828 +(c: int = 1,f"""{d=[ +def a( +class A: + pass diff --git a/crates/ruff_python_parser/src/token_source.rs b/crates/ruff_python_parser/src/token_source.rs index e5755806e3..e22196e00e 100644 --- a/crates/ruff_python_parser/src/token_source.rs +++ b/crates/ruff_python_parser/src/token_source.rs @@ -67,26 +67,59 @@ impl<'src> TokenSource<'src> { /// /// [`re_lex_logical_token`]: Lexer::re_lex_logical_token pub(crate) fn re_lex_logical_token(&mut self) { - let mut non_logical_newline_start = None; - for token in self.tokens.iter().rev() { + let mut non_logical_newline = None; + + #[cfg(debug_assertions)] + let last_non_trivia_end_before = { + self.tokens + .iter() + .rev() + .find(|tok| !tok.kind().is_trivia()) + .map(ruff_text_size::Ranged::end) + }; + + for (index, token) in self.tokens.iter().enumerate().rev() { match token.kind() { TokenKind::NonLogicalNewline => { - non_logical_newline_start = Some(token.start()); + non_logical_newline = Some((index, token.start())); } TokenKind::Comment => continue, _ => break, } } - if self.lexer.re_lex_logical_token(non_logical_newline_start) { - let current_start = self.current_range().start(); - while self - .tokens - .last() - .is_some_and(|last| last.start() >= current_start) - { - self.tokens.pop(); - } + if !self + .lexer + .re_lex_logical_token(non_logical_newline.map(|(_, start)| start)) + { + return; + } + + let non_logical_line_index = non_logical_newline + .expect( + "`re_lex_logical_token` should only return `true` if `non_logical_line` is `Some`", + ) + .0; + + // Trim the already bumped logical line token (and comments coming after it) as it might now have become a logical line token + self.tokens.truncate(non_logical_line_index); + + #[cfg(debug_assertions)] + { + let last_non_trivia_end_now = { + self.tokens + .iter() + .rev() + .find(|tok| !tok.kind().is_trivia()) + .map(ruff_text_size::Ranged::end) + }; + + assert_eq!(last_non_trivia_end_before, last_non_trivia_end_now); + } + + // Ensure `current` is positioned at a non-trivia token. + if self.current_kind().is_trivia() { + self.bump(self.current_kind()); } } diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 2c89ba7aad..a9378eddfe 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -4,15 +4,16 @@ use std::fmt::{Formatter, Write}; use std::fs; use std::path::Path; +use itertools::Itertools; use ruff_annotate_snippets::{Level, Renderer, Snippet}; -use ruff_python_ast::token::Token; +use ruff_python_ast::token::{Token, Tokens}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_module}; use ruff_python_ast::{self as ast, AnyNodeRef, Mod, PythonVersion}; use ruff_python_parser::semantic_errors::{ SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, }; -use ruff_python_parser::{Mode, ParseErrorType, ParseOptions, parse_unchecked}; +use ruff_python_parser::{Mode, ParseErrorType, ParseOptions, Parsed, parse_unchecked}; use ruff_source_file::{LineIndex, OneIndexed, SourceCode}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -81,7 +82,7 @@ fn test_valid_syntax(input_path: &Path) { } validate_tokens(parsed.tokens(), source.text_len(), input_path); - validate_ast(parsed.syntax(), source.text_len(), input_path); + validate_ast(&parsed, source.text_len(), input_path); let mut output = String::new(); writeln!(&mut output, "## AST").unwrap(); @@ -139,7 +140,7 @@ fn test_invalid_syntax(input_path: &Path) { let parsed = parse_unchecked(&source, options.clone()); validate_tokens(parsed.tokens(), source.text_len(), input_path); - validate_ast(parsed.syntax(), source.text_len(), input_path); + validate_ast(&parsed, source.text_len(), input_path); let mut output = String::new(); writeln!(&mut output, "## AST").unwrap(); @@ -402,12 +403,16 @@ Tokens: {tokens:#?} /// * the range of the parent node fully encloses all its child nodes /// * the ranges are strictly increasing when traversing the nodes in pre-order. /// * all ranges are within the length of the source code. -fn validate_ast(root: &Mod, source_len: TextSize, test_path: &Path) { - walk_module(&mut ValidateAstVisitor::new(source_len, test_path), root); +fn validate_ast(parsed: &Parsed, source_len: TextSize, test_path: &Path) { + walk_module( + &mut ValidateAstVisitor::new(parsed.tokens(), source_len, test_path), + parsed.syntax(), + ); } #[derive(Debug)] struct ValidateAstVisitor<'a> { + tokens: std::iter::Peekable>, parents: Vec>, previous: Option>, source_length: TextSize, @@ -415,8 +420,9 @@ struct ValidateAstVisitor<'a> { } impl<'a> ValidateAstVisitor<'a> { - fn new(source_length: TextSize, test_path: &'a Path) -> Self { + fn new(tokens: &'a Tokens, source_length: TextSize, test_path: &'a Path) -> Self { Self { + tokens: tokens.iter().peekable(), parents: Vec::new(), previous: None, source_length, @@ -425,6 +431,47 @@ impl<'a> ValidateAstVisitor<'a> { } } +impl ValidateAstVisitor<'_> { + /// Check that the node's start doesn't fall within a token. + /// Called in `enter_node` before visiting children. + fn assert_start_boundary(&mut self, node: AnyNodeRef<'_>) { + // Skip tokens that end at or before the node starts. + self.tokens + .peeking_take_while(|t| t.end() <= node.start()) + .last(); + + if let Some(next) = self.tokens.peek() { + // At this point, next_token.end() > node.start() + assert!( + next.start() >= node.start(), + "{path}: The start of the node falls within a token.\nNode: {node:#?}\n\nToken: {next:#?}\n\nRoot: {root:#?}", + path = self.test_path.display(), + root = self.parents.first() + ); + } + } + + /// Check that the node's end doesn't fall within a token. + /// Called in `leave_node` after visiting children, so all tokens + /// within the node have been consumed. + fn assert_end_boundary(&mut self, node: AnyNodeRef<'_>) { + // Skip tokens that end at or before the node ends. + self.tokens + .peeking_take_while(|t| t.end() <= node.end()) + .last(); + + if let Some(next) = self.tokens.peek() { + // At this point, `next_token.end() > node.end()` + assert!( + next.start() >= node.end(), + "{path}: The end of the node falls within a token.\nNode: {node:#?}\n\nToken: {next:#?}\n\nRoot: {root:#?}", + path = self.test_path.display(), + root = self.parents.first() + ); + } + } +} + impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { fn enter_node(&mut self, node: AnyNodeRef<'ast>) -> TraversalSignal { assert!( @@ -452,12 +499,16 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { ); } + self.assert_start_boundary(node); + self.parents.push(node); TraversalSignal::Traverse } fn leave_node(&mut self, node: AnyNodeRef<'ast>) { + self.assert_end_boundary(node); + self.parents.pop().expect("Expected tree to be balanced"); self.previous = Some(node); diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap index 61f3230855..93b1439a58 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap @@ -296,7 +296,7 @@ Module( test: Call( ExprCall { node_index: NodeIndex(None), - range: 456..472, + range: 456..471, func: Name( ExprName { node_index: NodeIndex(None), @@ -306,7 +306,7 @@ Module( }, ), arguments: Arguments { - range: 460..472, + range: 460..471, node_index: NodeIndex(None), args: [ Name( @@ -581,7 +581,7 @@ Module( test: Call( ExprCall { node_index: NodeIndex(None), - range: 890..906, + range: 890..905, func: Name( ExprName { node_index: NodeIndex(None), @@ -591,7 +591,7 @@ Module( }, ), arguments: Arguments { - range: 894..906, + range: 894..905, node_index: NodeIndex(None), args: [ FString( @@ -832,7 +832,16 @@ Module( | 28 | # The lexer is nested with multiple levels of parentheses 29 | if call(foo, [a, b - | ^ Syntax Error: Expected `]`, found NonLogicalNewline +30 | def bar(): + | ^^^ Syntax Error: Expected `]`, found `def` +31 | pass + | + + + | +28 | # The lexer is nested with multiple levels of parentheses +29 | if call(foo, [a, b + | ^ Syntax Error: Expected `)`, found newline 30 | def bar(): 31 | pass | @@ -857,11 +866,10 @@ Module( | -41 | # test is to make sure it emits a `NonLogicalNewline` token after `b`. 42 | if call(foo, [a, 43 | b - | ^ Syntax Error: Expected `]`, found NonLogicalNewline 44 | ) + | ^ Syntax Error: Expected `]`, found `)` 45 | def bar(): 46 | pass | @@ -898,7 +906,7 @@ Module( | 49 | # F-strings uses normal list parsing, so test those as well 50 | if call(f"hello {x - | ^ Syntax Error: Expected FStringEnd, found NonLogicalNewline + | ^ Syntax Error: Expected `)`, found newline 51 | def bar(): 52 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap index 5567459c70..2eb9515848 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap @@ -17,7 +17,7 @@ Module( test: Call( ExprCall { node_index: NodeIndex(None), - range: 3..19, + range: 3..18, func: Name( ExprName { node_index: NodeIndex(None), @@ -27,7 +27,7 @@ Module( }, ), arguments: Arguments { - range: 7..19, + range: 7..18, node_index: NodeIndex(None), args: [ Name( @@ -113,5 +113,11 @@ Module( | 1 | if call(foo, [a, b def bar(): pass - | ^ Syntax Error: Expected `]`, found NonLogicalNewline + | ^^^ Syntax Error: Expected `]`, found `def` + | + + + | +1 | if call(foo, [a, b def bar(): pass + | ^ Syntax Error: Expected `)`, found newline | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap index ae03fee095..692755d4e8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap @@ -17,7 +17,7 @@ Module( test: Call( ExprCall { node_index: NodeIndex(None), - range: 3..20, + range: 3..18, func: Name( ExprName { node_index: NodeIndex(None), @@ -27,7 +27,7 @@ Module( }, ), arguments: Arguments { - range: 7..20, + range: 7..18, node_index: NodeIndex(None), args: [ Name( @@ -113,7 +113,15 @@ Module( | 1 | if call(foo, [a, b - | ^ Syntax Error: Expected `]`, found NonLogicalNewline +2 | def bar(): + | ^^^ Syntax Error: Expected `]`, found `def` +3 | pass + | + + + | +1 | if call(foo, [a, b + | ^ Syntax Error: Expected `)`, found newline 2 | def bar(): 3 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap new file mode 100644 index 0000000000..49ea0c7d58 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap @@ -0,0 +1,227 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/invalid/re_lexing/ty_1828.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..112, + body: [ + AnnAssign( + StmtAnnAssign { + node_index: NodeIndex(None), + range: 66..93, + target: Name( + ExprName { + node_index: NodeIndex(None), + range: 67..68, + id: Name("c"), + ctx: Store, + }, + ), + annotation: Name( + ExprName { + node_index: NodeIndex(None), + range: 70..73, + id: Name("int"), + ctx: Load, + }, + ), + value: Some( + Tuple( + ExprTuple { + node_index: NodeIndex(None), + range: 76..93, + elts: [ + NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 76..77, + value: Int( + 1, + ), + }, + ), + Subscript( + ExprSubscript { + node_index: NodeIndex(None), + range: 78..90, + value: FString( + ExprFString { + node_index: NodeIndex(None), + range: 78..85, + value: FStringValue { + inner: Single( + FString( + FString { + range: 78..85, + node_index: NodeIndex(None), + elements: [ + Interpolation( + InterpolatedElement { + range: 82..85, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 83..84, + id: Name("d"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + unclosed: true, + }, + }, + ), + ), + }, + }, + ), + slice: Slice( + ExprSlice { + node_index: NodeIndex(None), + range: 87..90, + lower: None, + upper: Some( + Name( + ExprName { + node_index: NodeIndex(None), + range: 87..90, + id: Name("def"), + ctx: Load, + }, + ), + ), + step: None, + }, + ), + ctx: Load, + }, + ), + Call( + ExprCall { + node_index: NodeIndex(None), + range: 91..93, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 91..92, + id: Name("a"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 92..93, + node_index: NodeIndex(None), + args: [], + keywords: [], + }, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + simple: false, + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # Regression test for https://github.com/astral-sh/ty/issues/1828 +2 | (c: int = 1,f"""{d=[ + | ^ Syntax Error: Expected `)`, found `:` +3 | def a( +4 | class A: + | + + + | +1 | # Regression test for https://github.com/astral-sh/ty/issues/1828 +2 | (c: int = 1,f"""{d=[ + | ^ Syntax Error: f-string: expecting `}` +3 | def a( +4 | class A: + | + + + | +1 | # Regression test for https://github.com/astral-sh/ty/issues/1828 +2 | (c: int = 1,f"""{d=[ +3 | def a( + | ^^^ Syntax Error: Expected `:`, found `def` +4 | class A: +5 | pass + | + + + | +1 | # Regression test for https://github.com/astral-sh/ty/issues/1828 +2 | (c: int = 1,f"""{d=[ +3 | def a( + | ^ Syntax Error: Expected `]`, found name +4 | class A: +5 | pass + | + + + | +1 | # Regression test for https://github.com/astral-sh/ty/issues/1828 +2 | (c: int = 1,f"""{d=[ +3 | def a( + | _______^ +4 | | class A: +5 | | pass + | |_________^ Syntax Error: f-string: unterminated triple-quoted string + | + + + | +2 | (c: int = 1,f"""{d=[ +3 | def a( +4 | class A: + | ^^^^^ Syntax Error: Expected `)`, found `class` +5 | pass + | + + + | +1 | # Regression test for https://github.com/astral-sh/ty/issues/1828 +2 | (c: int = 1,f"""{d=[ +3 | def a( + | _______^ +4 | | class A: +5 | | pass + | |_________^ Syntax Error: Expected a statement + | + + + | +4 | class A: +5 | pass + | ^ Syntax Error: unexpected EOF while parsing + | diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index b03b1930ca..ca2305df0c 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -6433,6 +6433,155 @@ collabc assert_snapshot!(snapshot, @"collections.abc"); } + #[test] + fn local_function_variable_with_return() { + let builder = completion_test_builder( + "\ +variable_global = 1 +def foo(): + variable_local = 1 + variable_ + return +", + ); + assert_snapshot!( + builder.skip_auto_import().build().snapshot(), + @r" + variable_global + variable_local + ", + ); + } + + #[test] + fn nested_scopes_with_return() { + let builder = completion_test_builder( + "\ +variable_1 = 1 +def fun1(): + variable_2 = 1 + def fun2(): + variable_3 = 1 + def fun3(): + variable_4 = 1 + variable_ + return + return + return +", + ); + assert_snapshot!( + builder.skip_auto_import().build().snapshot(), + @r" + variable_1 + variable_2 + variable_3 + variable_4 + ", + ); + } + + #[test] + fn multiple_declarations_global_scope1() { + let builder = completion_test_builder( + "\ +zqzqzq: int = 1 +zqzqzq: str = 'foo' +zqzq +", + ); + // The type for `zqzqzq` *should* be `str`, but we consider all + // reachable declarations and bindings, which means we get a + // union of `int` and `str` here even though the `int` binding + // isn't live at the cursor position. + assert_snapshot!( + builder.skip_auto_import().type_signatures().build().snapshot(), + @"zqzqzq :: int | str", + ); + } + + #[test] + fn multiple_declarations_global_scope2() { + let builder = completion_test_builder( + "\ +zqzqzq: int = 1 +zqzq +zqzqzq: str = 'foo' +", + ); + // The type for `zqzqzq` *should* be `int`, but we consider all + // reachable declarations and bindings, which means we get a + // union of `int` and `str` here even though the `str` binding + // doesn't exist at the cursor position. + assert_snapshot!( + builder.skip_auto_import().type_signatures().build().snapshot(), + @"zqzqzq :: int | str", + ); + } + + #[test] + fn multiple_declarations_function_scope1() { + let builder = completion_test_builder( + "\ +def foo(): + zqzqzq: int = 1 + zqzqzq: str = 'foo' + zqzq + return +", + ); + // The type for `zqzqzq` *should* be `str`, but we consider all + // reachable declarations and bindings, which means we get a + // union of `int` and `str` here even though the `int` binding + // isn't live at the cursor position. + assert_snapshot!( + builder.skip_auto_import().type_signatures().build().snapshot(), + @"zqzqzq :: int | str", + ); + } + + #[test] + fn multiple_declarations_function_scope2() { + let builder = completion_test_builder( + "\ +def foo(): + zqzqzq: int = 1 + zqzq + zqzqzq: str = 'foo' + return +", + ); + // The type for `zqzqzq` *should* be `int`, but we consider all + // reachable declarations and bindings, which means we get a + // union of `int` and `str` here even though the `str` binding + // doesn't exist at the cursor position. + assert_snapshot!( + builder.skip_auto_import().type_signatures().build().snapshot(), + @"zqzqzq :: int | str", + ); + } + + #[test] + fn multiple_declarations_function_parameter() { + let builder = completion_test_builder( + "\ +from pathlib import Path +def f(zqzqzq: str): + zqzqzq: Path = Path(zqzqzq) + zqzq + return +", + ); + // The type for `zqzqzq` *should* be `Path`, but we consider all + // reachable declarations and bindings, which means we get a + // union of `str` and `Path` here even though the `str` binding + // isn't live at the cursor position. + assert_snapshot!( + builder.skip_auto_import().type_signatures().build().snapshot(), + @"zqzqzq :: str | Path", + ); + } + /// A way to create a simple single-file (named `main.py`) completion test /// builder. /// diff --git a/crates/ty_ide/src/find_references.rs b/crates/ty_ide/src/find_references.rs index e62608e555..274279c4e5 100644 --- a/crates/ty_ide/src/find_references.rs +++ b/crates/ty_ide/src/find_references.rs @@ -35,8 +35,6 @@ mod tests { use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; use insta::assert_snapshot; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span}; - use ruff_db::files::FileRange; - use ruff_text_size::Ranged; impl CursorTest { fn references(&self) -> String { @@ -52,20 +50,14 @@ mod tests { reference_results.sort_by_key(ReferenceTarget::file); - self.render_diagnostics(reference_results.into_iter().enumerate().map( - |(i, ref_item)| -> ReferenceResult { - ReferenceResult { - index: i, - file_range: FileRange::new(ref_item.file(), ref_item.range()), - } - }, - )) + self.render_diagnostics([ReferenceResult { + references: reference_results, + }]) } } struct ReferenceResult { - index: usize, - file_range: FileRange, + references: Vec, } impl IntoDiagnostic for ReferenceResult { @@ -73,11 +65,14 @@ mod tests { let mut main = Diagnostic::new( DiagnosticId::Lint(LintName::of("references")), Severity::Info, - format!("Reference {}", self.index + 1), + format!("Found {} references", self.references.len()), ); - main.annotate(Annotation::primary( - Span::from(self.file_range.file()).with_range(self.file_range.range()), - )); + + for reference in self.references { + main.annotate(Annotation::secondary( + Span::from(reference.file()).with_range(reference.range()), + )); + } main } @@ -97,55 +92,24 @@ result = calculate_sum(value=42) ", ); - assert_snapshot!(test.references(), @r###" - info[references]: Reference 1 + assert_snapshot!(test.references(), @r" + info[references]: Found 5 references --> main.py:2:19 | 2 | def calculate_sum(value: int) -> int: - | ^^^^^ + | ----- 3 | doubled = value * 2 + | ----- 4 | result = value + doubled - | - - info[references]: Reference 2 - --> main.py:3:15 - | - 2 | def calculate_sum(value: int) -> int: - 3 | doubled = value * 2 - | ^^^^^ - 4 | result = value + doubled + | ----- 5 | return value - | - - info[references]: Reference 3 - --> main.py:4:14 - | - 2 | def calculate_sum(value: int) -> int: - 3 | doubled = value * 2 - 4 | result = value + doubled - | ^^^^^ - 5 | return value - | - - info[references]: Reference 4 - --> main.py:5:12 - | - 3 | doubled = value * 2 - 4 | result = value + doubled - 5 | return value - | ^^^^^ + | ----- 6 | - 7 | # Call with keyword argument - | - - info[references]: Reference 5 - --> main.py:8:24 - | 7 | # Call with keyword argument 8 | result = calculate_sum(value=42) - | ^^^^^ + | ----- | - "###); + "); } #[test] @@ -176,95 +140,36 @@ def outer_function(): ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> main.py:3:5 - | - 2 | def outer_function(): - 3 | counter = 0 - | ^^^^^^^ - 4 | - 5 | def increment(): - | - - info[references]: Reference 2 - --> main.py:6:18 - | - 5 | def increment(): - 6 | nonlocal counter - | ^^^^^^^ - 7 | counter += 1 - 8 | return counter - | - - info[references]: Reference 3 - --> main.py:7:9 - | - 5 | def increment(): - 6 | nonlocal counter - 7 | counter += 1 - | ^^^^^^^ - 8 | return counter - | - - info[references]: Reference 4 - --> main.py:8:16 + info[references]: Found 9 references + --> main.py:3:5 | + 2 | def outer_function(): + 3 | counter = 0 + | ------- + 4 | + 5 | def increment(): 6 | nonlocal counter + | ------- 7 | counter += 1 + | ------- 8 | return counter - | ^^^^^^^ + | ------- 9 | - 10 | def decrement(): - | - - info[references]: Reference 5 - --> main.py:11:18 - | 10 | def decrement(): 11 | nonlocal counter - | ^^^^^^^ + | ------- 12 | counter -= 1 + | ------- 13 | return counter - | - - info[references]: Reference 6 - --> main.py:12:9 - | - 10 | def decrement(): - 11 | nonlocal counter - 12 | counter -= 1 - | ^^^^^^^ - 13 | return counter - | - - info[references]: Reference 7 - --> main.py:13:16 - | - 11 | nonlocal counter - 12 | counter -= 1 - 13 | return counter - | ^^^^^^^ + | ------- 14 | - 15 | # Use counter in outer scope - | - - info[references]: Reference 8 - --> main.py:16:15 - | 15 | # Use counter in outer scope 16 | initial = counter - | ^^^^^^^ - 17 | increment() - 18 | decrement() - | - - info[references]: Reference 9 - --> main.py:19:13 - | + | ------- 17 | increment() 18 | decrement() 19 | final = counter - | ^^^^^^^ + | ------- 20 | 21 | return increment, decrement | @@ -296,94 +201,35 @@ final_value = global_counter ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> main.py:2:1 - | - 2 | global_counter = 0 - | ^^^^^^^^^^^^^^ - 3 | - 4 | def increment_global(): - | - - info[references]: Reference 2 - --> main.py:5:12 - | - 4 | def increment_global(): - 5 | global global_counter - | ^^^^^^^^^^^^^^ - 6 | global_counter += 1 - 7 | return global_counter - | - - info[references]: Reference 3 - --> main.py:6:5 - | - 4 | def increment_global(): - 5 | global global_counter - 6 | global_counter += 1 - | ^^^^^^^^^^^^^^ - 7 | return global_counter - | - - info[references]: Reference 4 - --> main.py:7:12 - | - 5 | global global_counter - 6 | global_counter += 1 - 7 | return global_counter - | ^^^^^^^^^^^^^^ - 8 | - 9 | def decrement_global(): - | - - info[references]: Reference 5 - --> main.py:10:12 + info[references]: Found 9 references + --> main.py:2:1 | + 2 | global_counter = 0 + | -------------- + 3 | + 4 | def increment_global(): + 5 | global global_counter + | -------------- + 6 | global_counter += 1 + | -------------- + 7 | return global_counter + | -------------- + 8 | 9 | def decrement_global(): 10 | global global_counter - | ^^^^^^^^^^^^^^ + | -------------- 11 | global_counter -= 1 + | -------------- 12 | return global_counter - | - - info[references]: Reference 6 - --> main.py:11:5 - | - 9 | def decrement_global(): - 10 | global global_counter - 11 | global_counter -= 1 - | ^^^^^^^^^^^^^^ - 12 | return global_counter - | - - info[references]: Reference 7 - --> main.py:12:12 - | - 10 | global global_counter - 11 | global_counter -= 1 - 12 | return global_counter - | ^^^^^^^^^^^^^^ + | -------------- 13 | - 14 | # Use global_counter at module level - | - - info[references]: Reference 8 - --> main.py:15:17 - | 14 | # Use global_counter at module level 15 | initial_value = global_counter - | ^^^^^^^^^^^^^^ - 16 | increment_global() - 17 | decrement_global() - | - - info[references]: Reference 9 - --> main.py:18:15 - | + | -------------- 16 | increment_global() 17 | decrement_global() 18 | final_value = global_counter - | ^^^^^^^^^^^^^^ + | -------------- | "); } @@ -406,45 +252,23 @@ except ValueError as err: ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> main.py:4:29 - | - 2 | try: - 3 | x = 1 / 0 - 4 | except ZeroDivisionError as err: - | ^^^ - 5 | print(f'Error: {err}') - 6 | return err - | - - info[references]: Reference 2 - --> main.py:5:21 - | - 3 | x = 1 / 0 - 4 | except ZeroDivisionError as err: - 5 | print(f'Error: {err}') - | ^^^ - 6 | return err - | - - info[references]: Reference 3 - --> main.py:6:12 - | - 4 | except ZeroDivisionError as err: - 5 | print(f'Error: {err}') - 6 | return err - | ^^^ - 7 | - 8 | try: - | - - info[references]: Reference 4 - --> main.py:11:31 + info[references]: Found 4 references + --> main.py:4:29 | + 2 | try: + 3 | x = 1 / 0 + 4 | except ZeroDivisionError as err: + | --- + 5 | print(f'Error: {err}') + | --- + 6 | return err + | --- + 7 | + 8 | try: 9 | y = 2 / 0 10 | except ValueError as err: 11 | print(f'Different error: {err}') - | ^^^ + | --- | "); } @@ -462,39 +286,21 @@ match x: ", ); - assert_snapshot!(test.references(), @r###" - info[references]: Reference 1 + assert_snapshot!(test.references(), @r" + info[references]: Found 3 references --> main.py:3:20 | 2 | match x: 3 | case [a, b] as pattern: - | ^^^^^^^ + | ------- 4 | print(f'Matched: {pattern}') + | ------- 5 | return pattern - | - - info[references]: Reference 2 - --> main.py:4:27 - | - 2 | match x: - 3 | case [a, b] as pattern: - 4 | print(f'Matched: {pattern}') - | ^^^^^^^ - 5 | return pattern - 6 | case _: - | - - info[references]: Reference 3 - --> main.py:5:16 - | - 3 | case [a, b] as pattern: - 4 | print(f'Matched: {pattern}') - 5 | return pattern - | ^^^^^^^ + | ------- 6 | case _: 7 | pass | - "###); + "); } #[test] @@ -509,47 +315,21 @@ match data: ", ); - assert_snapshot!(test.references(), @r###" - info[references]: Reference 1 + assert_snapshot!(test.references(), @r" + info[references]: Found 4 references --> main.py:3:29 | 2 | match data: 3 | case {'a': a, 'b': b, **rest}: - | ^^^^ + | ---- 4 | print(f'Rest data: {rest}') + | ---- 5 | process(rest) - | - - info[references]: Reference 2 - --> main.py:4:29 - | - 2 | match data: - 3 | case {'a': a, 'b': b, **rest}: - 4 | print(f'Rest data: {rest}') - | ^^^^ - 5 | process(rest) + | ---- 6 | return rest + | ---- | - - info[references]: Reference 3 - --> main.py:5:17 - | - 3 | case {'a': a, 'b': b, **rest}: - 4 | print(f'Rest data: {rest}') - 5 | process(rest) - | ^^^^ - 6 | return rest - | - - info[references]: Reference 4 - --> main.py:6:16 - | - 4 | print(f'Rest data: {rest}') - 5 | process(rest) - 6 | return rest - | ^^^^ - | - "###); + "); } #[test] @@ -573,60 +353,30 @@ value = my_function ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> main.py:2:5 - | - 2 | def my_function(): - | ^^^^^^^^^^^ - 3 | return 42 - | - - info[references]: Reference 2 - --> main.py:6:11 - | - 5 | # Call the function multiple times - 6 | result1 = my_function() - | ^^^^^^^^^^^ - 7 | result2 = my_function() - | - - info[references]: Reference 3 - --> main.py:7:11 - | - 5 | # Call the function multiple times - 6 | result1 = my_function() - 7 | result2 = my_function() - | ^^^^^^^^^^^ - 8 | - 9 | # Function passed as an argument - | - - info[references]: Reference 4 - --> main.py:10:12 + info[references]: Found 6 references + --> main.py:2:5 | + 2 | def my_function(): + | ----------- + 3 | return 42 + | + ::: main.py:6:11 + | + 5 | # Call the function multiple times + 6 | result1 = my_function() + | ----------- + 7 | result2 = my_function() + | ----------- + 8 | 9 | # Function passed as an argument 10 | callback = my_function - | ^^^^^^^^^^^ + | ----------- 11 | - 12 | # Function used in different contexts - | - - info[references]: Reference 5 - --> main.py:13:7 - | 12 | # Function used in different contexts 13 | print(my_function()) - | ^^^^^^^^^^^ + | ----------- 14 | value = my_function - | - - info[references]: Reference 6 - --> main.py:14:9 - | - 12 | # Function used in different contexts - 13 | print(my_function()) - 14 | value = my_function - | ^^^^^^^^^^^ + | ----------- | "); } @@ -653,59 +403,32 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> main.py:2:7 - | - 2 | class MyClass: - | ^^^^^^^ - 3 | def __init__(self): - 4 | pass - | - - info[references]: Reference 2 - --> main.py:7:8 - | - 6 | # Create instances - 7 | obj1 = MyClass() - | ^^^^^^^ - 8 | obj2 = MyClass() - | - - info[references]: Reference 3 - --> main.py:8:8 + info[references]: Found 6 references + --> main.py:2:7 + | + 2 | class MyClass: + | ------- + 3 | def __init__(self): + 4 | pass + | + ::: main.py:7:8 | 6 | # Create instances 7 | obj1 = MyClass() + | ------- 8 | obj2 = MyClass() - | ^^^^^^^ + | ------- 9 | - 10 | # Use in type annotations - | - - info[references]: Reference 4 - --> main.py:11:23 - | 10 | # Use in type annotations 11 | def process(instance: MyClass) -> MyClass: - | ^^^^^^^ + | ------- ------- 12 | return instance | - - info[references]: Reference 5 - --> main.py:11:35 - | - 10 | # Use in type annotations - 11 | def process(instance: MyClass) -> MyClass: - | ^^^^^^^ - 12 | return instance - | - - info[references]: Reference 6 - --> main.py:15:7 + ::: main.py:15:7 | 14 | # Reference the class itself 15 | cls = MyClass - | ^^^^^^^ + | ------- | "); } @@ -722,22 +445,14 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:5 | 2 | a: "MyClass" = 1 - | ^^^^^^^ + | ------- 3 | 4 | class MyClass: - | - - info[references]: Reference 2 - --> main.py:4:7 - | - 2 | a: "MyClass" = 1 - 3 | - 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | "#); @@ -755,22 +470,14 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:12 | 2 | a: "None | MyClass" = 1 - | ^^^^^^^ + | ------- 3 | 4 | class MyClass: - | - - info[references]: Reference 2 - --> main.py:4:7 - | - 2 | a: "None | MyClass" = 1 - 3 | - 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | "#); @@ -802,22 +509,14 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:12 | 2 | a: "None | MyClass" = 1 - | ^^^^^^^ + | ------- 3 | 4 | class MyClass: - | - - info[references]: Reference 2 - --> main.py:4:7 - | - 2 | a: "None | MyClass" = 1 - 3 | - 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | "#); @@ -863,22 +562,14 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:5 | 2 | a: "MyClass | No" = 1 - | ^^^^^^^ + | ------- 3 | 4 | class MyClass: - | - - info[references]: Reference 2 - --> main.py:4:7 - | - 2 | a: "MyClass | No" = 1 - 3 | - 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | "#); @@ -907,18 +598,11 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:1 | 2 | ab: "ab" - | ^^ - | - - info[references]: Reference 2 - --> main.py:2:6 - | - 2 | ab: "ab" - | ^^ + | -- -- | "#); } @@ -946,23 +630,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:4:22 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ab]: - | ^^ + | -- 5 | x = ab - | - - info[references]: Reference 2 - --> main.py:5:17 - | - 3 | match command.split(): - 4 | case ["get", ab]: - 5 | x = ab - | ^^ + | -- | "#); } @@ -979,23 +655,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:4:22 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ab]: - | ^^ + | -- 5 | x = ab - | - - info[references]: Reference 2 - --> main.py:5:17 - | - 3 | match command.split(): - 4 | case ["get", ab]: - 5 | x = ab - | ^^ + | -- | "#); } @@ -1012,23 +680,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:4:23 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", *ab]: - | ^^ + | -- 5 | x = ab - | - - info[references]: Reference 2 - --> main.py:5:17 - | - 3 | match command.split(): - 4 | case ["get", *ab]: - 5 | x = ab - | ^^ + | -- | "#); } @@ -1045,23 +705,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:4:23 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", *ab]: - | ^^ + | -- 5 | x = ab - | - - info[references]: Reference 2 - --> main.py:5:17 - | - 3 | match command.split(): - 4 | case ["get", *ab]: - 5 | x = ab - | ^^ + | -- | "#); } @@ -1078,23 +730,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:4:37 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ("a" | "b") as ab]: - | ^^ + | -- 5 | x = ab - | - - info[references]: Reference 2 - --> main.py:5:17 - | - 3 | match command.split(): - 4 | case ["get", ("a" | "b") as ab]: - 5 | x = ab - | ^^ + | -- | "#); } @@ -1111,23 +755,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:4:37 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ("a" | "b") as ab]: - | ^^ + | -- 5 | x = ab - | - - info[references]: Reference 2 - --> main.py:5:17 - | - 3 | match command.split(): - 4 | case ["get", ("a" | "b") as ab]: - 5 | x = ab - | ^^ + | -- | "#); } @@ -1150,23 +786,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:10:30 | 8 | def my_func(event: Click): 9 | match event: 10 | case Click(x, button=ab): - | ^^ + | -- 11 | x = ab - | - - info[references]: Reference 2 - --> main.py:11:17 - | - 9 | match event: - 10 | case Click(x, button=ab): - 11 | x = ab - | ^^ + | -- | "); } @@ -1189,23 +817,15 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:10:30 | 8 | def my_func(event: Click): 9 | match event: 10 | case Click(x, button=ab): - | ^^ + | -- 11 | x = ab - | - - info[references]: Reference 2 - --> main.py:11:17 - | - 9 | match event: - 10 | case Click(x, button=ab): - 11 | x = ab - | ^^ + | -- | "); } @@ -1228,33 +848,23 @@ cls = MyClass ); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 - --> main.py:2:7 - | - 2 | class Click: - | ^^^^^ - 3 | __match_args__ = ("position", "button") - 4 | def __init__(self, pos, btn): - | - - info[references]: Reference 2 - --> main.py:8:20 + info[references]: Found 3 references + --> main.py:2:7 + | + 2 | class Click: + | ----- + 3 | __match_args__ = ("position", "button") + 4 | def __init__(self, pos, btn): + | + ::: main.py:8:20 | 6 | self.button: str = btn 7 | 8 | def my_func(event: Click): - | ^^^^^ + | ----- 9 | match event: 10 | case Click(x, button=ab): - | - - info[references]: Reference 3 - --> main.py:10:14 - | - 8 | def my_func(event: Click): - 9 | match event: - 10 | case Click(x, button=ab): - | ^^^^^ + | ----- 11 | x = ab | "#); @@ -1289,25 +899,11 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ - | - - info[references]: Reference 2 - --> main.py:2:37 - | - 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ - | - - info[references]: Reference 3 - --> main.py:2:46 - | - 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | -- -- -- | "); } @@ -1321,25 +917,11 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ - | - - info[references]: Reference 2 - --> main.py:2:37 - | - 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ - | - - info[references]: Reference 3 - --> main.py:2:46 - | - 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | -- -- -- | "); } @@ -1354,28 +936,12 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:3:15 | 2 | from typing import Callable 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ - | - - info[references]: Reference 2 - --> main.py:3:43 - | - 2 | from typing import Callable - 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ - | - - info[references]: Reference 3 - --> main.py:3:53 - | - 2 | from typing import Callable - 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ + | -- -- -- | "); } @@ -1390,28 +956,12 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:3:15 | 2 | from typing import Callable 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ - | - - info[references]: Reference 2 - --> main.py:3:43 - | - 2 | from typing import Callable - 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ - | - - info[references]: Reference 3 - --> main.py:3:53 - | - 2 | from typing import Callable - 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ + | -- -- -- | "); } @@ -1425,25 +975,11 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ - | - - info[references]: Reference 2 - --> main.py:2:38 - | - 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ - | - - info[references]: Reference 3 - --> main.py:2:50 - | - 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ + | -- -- -- | "); } @@ -1457,25 +993,11 @@ cls = MyClass ); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ - | - - info[references]: Reference 2 - --> main.py:2:38 - | - 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ - | - - info[references]: Reference 3 - --> main.py:2:50 - | - 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ + | -- -- -- | "); } @@ -1515,57 +1037,35 @@ class DataProcessor: .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> utils.py:2:5 - | - 2 | def func(x): - | ^^^^ - 3 | return x * 2 - | - - info[references]: Reference 2 - --> module.py:2:19 - | - 2 | from utils import func - | ^^^^ - 3 | - 4 | def process_data(data): - | - - info[references]: Reference 3 - --> module.py:5:12 - | - 4 | def process_data(data): - 5 | return func(data) - | ^^^^ - | - - info[references]: Reference 4 + info[references]: Found 6 references --> app.py:2:19 | 2 | from utils import func - | ^^^^ + | ---- 3 | - 4 | class DataProcessor: - | - - info[references]: Reference 5 - --> app.py:6:27 - | 4 | class DataProcessor: 5 | def __init__(self): 6 | self.multiplier = func - | ^^^^ + | ---- 7 | - 8 | def process(self, value): - | - - info[references]: Reference 6 - --> app.py:9:16 - | 8 | def process(self, value): 9 | return func(value) - | ^^^^ + | ---- + | + ::: module.py:2:19 + | + 2 | from utils import func + | ---- + 3 | + 4 | def process_data(data): + 5 | return func(data) + | ---- + | + ::: utils.py:2:5 + | + 2 | def func(x): + | ---- + 3 | return x * 2 | "); } @@ -1598,52 +1098,27 @@ def process_model(): .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 - --> models.py:3:5 - | - 2 | class MyModel: - 3 | attr = 42 - | ^^^^ - 4 | - 5 | def get_attribute(self): - | - - info[references]: Reference 2 - --> models.py:6:24 - | - 5 | def get_attribute(self): - 6 | return MyModel.attr - | ^^^^ - | - - info[references]: Reference 3 + info[references]: Found 5 references --> main.py:6:19 | 4 | def process_model(): 5 | model = MyModel() 6 | value = model.attr - | ^^^^ + | ---- 7 | model.attr = 100 + | ---- 8 | return model.attr + | ---- | - - info[references]: Reference 4 - --> main.py:7:11 + ::: models.py:3:5 | - 5 | model = MyModel() - 6 | value = model.attr - 7 | model.attr = 100 - | ^^^^ - 8 | return model.attr - | - - info[references]: Reference 5 - --> main.py:8:18 - | - 6 | value = model.attr - 7 | model.attr = 100 - 8 | return model.attr - | ^^^^ + 2 | class MyModel: + 3 | attr = 42 + | ---- + 4 | + 5 | def get_attribute(self): + 6 | return MyModel.attr + | ---- | "); } @@ -1673,22 +1148,14 @@ func_alias() // When finding references to the alias, we should NOT find references // to the original function in the original module assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> importer.py:2:30 | 2 | from original import func as func_alias - | ^^^^^^^^^^ + | ---------- 3 | 4 | func_alias() - | - - info[references]: Reference 2 - --> importer.py:4:1 - | - 2 | from original import func as func_alias - 3 | - 4 | func_alias() - | ^^^^^^^^^^ + | ---------- | "); } @@ -1721,42 +1188,23 @@ func_alias() ) .build(); - assert_snapshot!(test.references(), @r###" - info[references]: Reference 1 - --> path.pyi:2:7 - | - 2 | class Path: - | ^^^^ - 3 | def __init__(self, path: str): ... - | - - info[references]: Reference 2 + assert_snapshot!(test.references(), @r#" + info[references]: Found 4 references --> importer.py:2:18 | 2 | from path import Path - | ^^^^ + | ---- 3 | 4 | a: Path = Path("test") + | ---- ---- | - - info[references]: Reference 3 - --> importer.py:4:4 + ::: path.pyi:2:7 | - 2 | from path import Path - 3 | - 4 | a: Path = Path("test") - | ^^^^ + 2 | class Path: + | ---- + 3 | def __init__(self, path: str): ... | - - info[references]: Reference 4 - --> importer.py:4:11 - | - 2 | from path import Path - 3 | - 4 | a: Path = Path("test") - | ^^^^ - | - "###); + "#); } #[test] @@ -1775,23 +1223,15 @@ func_alias() .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:3:20 | 2 | import warnings 3 | import warnings as abc - | ^^^ + | --- 4 | 5 | x = abc - | - - info[references]: Reference 2 - --> main.py:5:5 - | - 3 | import warnings as abc - 4 | - 5 | x = abc - | ^^^ + | --- 6 | y = warnings | "); @@ -1813,23 +1253,15 @@ func_alias() .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:3:20 | 2 | import warnings 3 | import warnings as abc - | ^^^ + | --- 4 | 5 | x = abc - | - - info[references]: Reference 2 - --> main.py:5:5 - | - 3 | import warnings as abc - 4 | - 5 | x = abc - | ^^^ + | --- 6 | y = warnings | "); @@ -1851,21 +1283,15 @@ func_alias() .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:36 | 2 | from warnings import deprecated as xyz - | ^^^ - 3 | from warnings import deprecated - | - - info[references]: Reference 2 - --> main.py:5:5 - | + | --- 3 | from warnings import deprecated 4 | 5 | y = xyz - | ^^^ + | --- 6 | z = deprecated | "); @@ -1887,21 +1313,15 @@ func_alias() .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 2 references --> main.py:2:36 | 2 | from warnings import deprecated as xyz - | ^^^ - 3 | from warnings import deprecated - | - - info[references]: Reference 2 - --> main.py:5:5 - | + | --- 3 | from warnings import deprecated 4 | 5 | y = xyz - | ^^^ + | --- 6 | z = deprecated | "); @@ -1929,13 +1349,13 @@ func_alias() // TODO(submodule-imports): this should light up both instances of `subpkg` assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 1 references --> mypackage/__init__.py:4:5 | 2 | from .subpkg.submod import val 3 | 4 | x = subpkg - | ^^^^^^ + | ------ | "); } @@ -2055,29 +1475,19 @@ func_alias() .build(); assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 3 references --> mypackage/__init__.py:2:21 | 2 | from .subpkg import subpkg - | ^^^^^^ + | ------ 3 | 4 | x = subpkg + | ------ | - - info[references]: Reference 2 - --> mypackage/__init__.py:4:5 - | - 2 | from .subpkg import subpkg - 3 | - 4 | x = subpkg - | ^^^^^^ - | - - info[references]: Reference 3 - --> mypackage/subpkg/__init__.py:2:1 + ::: mypackage/subpkg/__init__.py:2:1 | 2 | subpkg: int = 10 - | ^^^^^^ + | ------ | "); } @@ -2103,13 +1513,13 @@ func_alias() // TODO: this should also highlight the RHS subpkg in the import assert_snapshot!(test.references(), @r" - info[references]: Reference 1 + info[references]: Found 1 references --> mypackage/__init__.py:4:5 | 2 | from .subpkg import subpkg 3 | 4 | x = subpkg - | ^^^^^^ + | ------ | "); } @@ -2131,33 +1541,17 @@ func_alias() .build(); assert_snapshot!(test.references(), @r#" - info[references]: Reference 1 + info[references]: Found 3 references --> main.py:2:1 | 2 | a: str = "test" - | ^ + | - 3 | 4 | a: int = 10 - | - - info[references]: Reference 2 - --> main.py:4:1 - | - 2 | a: str = "test" - 3 | - 4 | a: int = 10 - | ^ + | - 5 | 6 | print(a) - | - - info[references]: Reference 3 - --> main.py:6:7 - | - 4 | a: int = 10 - 5 | - 6 | print(a) - | ^ + | - | "#); } diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 217f8b420b..e0f298024d 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -295,7 +295,7 @@ impl<'db> Definitions<'db> { impl GotoTarget<'_> { pub(crate) fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { - let ty = match self { + match self { GotoTarget::Expression(expression) => expression.inferred_type(model), GotoTarget::FunctionDef(function) => function.inferred_type(model), GotoTarget::ClassDef(class) => class.inferred_type(model), @@ -317,7 +317,7 @@ impl GotoTarget<'_> { } => { // We don't currently support hovering the bare `.` so there is always a name let module = import_name(module_name, *component_index); - model.resolve_module_type(Some(module), *level)? + model.resolve_module_type(Some(module), *level) } GotoTarget::StringAnnotationSubexpr { string_expr, @@ -334,16 +334,16 @@ impl GotoTarget<'_> { } else { // TODO: force the typechecker to tell us its secrets // (it computes but then immediately discards these types) - return None; + None } } GotoTarget::BinOp { expression, .. } => { let (_, ty) = ty_python_semantic::definitions_for_bin_op(model, expression)?; - ty + Some(ty) } GotoTarget::UnaryOp { expression, .. } => { let (_, ty) = ty_python_semantic::definitions_for_unary_op(model, expression)?; - ty + Some(ty) } // TODO: Support identifier targets GotoTarget::PatternMatchRest(_) @@ -353,10 +353,8 @@ impl GotoTarget<'_> { | GotoTarget::TypeParamParamSpecName(_) | GotoTarget::TypeParamTypeVarTupleName(_) | GotoTarget::NonLocal { .. } - | GotoTarget::Globals { .. } => return None, - }; - - Some(ty) + | GotoTarget::Globals { .. } => None, + } } /// Try to get a simplified display of this callable type by resolving overloads diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index 8425654e19..2e390711b7 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -31,15 +31,9 @@ pub fn goto_declaration( #[cfg(test)] mod tests { - use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; - use crate::{NavigationTarget, goto_declaration}; + use crate::goto_declaration; + use crate::tests::{CursorTest, cursor_test}; use insta::assert_snapshot; - use ruff_db::diagnostic::{ - Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, - SubDiagnosticSeverity, - }; - use ruff_db::files::FileRange; - use ruff_text_size::Ranged; #[test] fn goto_declaration_function_call_to_definition() { @@ -53,20 +47,20 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:5 - | - 2 | def my_function(x, y): - | ^^^^^^^^^^^ - 3 | return x + y - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:10 | 3 | return x + y 4 | 5 | result = my_function(1, 2) - | ^^^^^^^^^^^ + | ^^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> main.py:2:5 + | + 2 | def my_function(x, y): + | ----------- + 3 | return x + y | "); } @@ -81,19 +75,19 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:1 - | - 2 | x = 42 - | ^ - 3 | y = x - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:5 | 2 | x = 42 3 | y = x - | ^ + | ^ Clicking here + | + info: Found 1 declaration + --> main.py:2:1 + | + 2 | x = 42 + | - + 3 | y = x | "); } @@ -111,39 +105,23 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:6:12 + | + 4 | pass + 5 | + 6 | instance = MyClass() + | ^^^^^^^ Clicking here + | + info: Found 2 declarations --> main.py:2:7 | 2 | class MyClass: - | ^^^^^^^ + | ------- 3 | def __init__(self): + | -------- 4 | pass | - info: Source - --> main.py:6:12 - | - 4 | pass - 5 | - 6 | instance = MyClass() - | ^^^^^^^ - | - - info[goto-declaration]: Declaration - --> main.py:3:9 - | - 2 | class MyClass: - 3 | def __init__(self): - | ^^^^^^^^ - 4 | pass - | - info: Source - --> main.py:6:12 - | - 4 | pass - 5 | - 6 | instance = MyClass() - | ^^^^^^^ - | "); } @@ -157,19 +135,19 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:9 - | - 2 | def foo(param): - | ^^^^^ - 3 | return param * 2 - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:12 | 2 | def foo(param): 3 | return param * 2 - | ^^^^^ + | ^^^^^ Clicking here + | + info: Found 1 declaration + --> main.py:2:9 + | + 2 | def foo(param): + | ----- + 3 | return param * 2 | "); } @@ -185,20 +163,20 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:18 - | - 2 | def generic_func[T](value: T) -> T: - | ^ - 3 | v: T = value - 4 | return v - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:8 | 2 | def generic_func[T](value: T) -> T: 3 | v: T = value - | ^ + | ^ Clicking here + 4 | return v + | + info: Found 1 declaration + --> main.py:2:18 + | + 2 | def generic_func[T](value: T) -> T: + | - + 3 | v: T = value 4 | return v | "); @@ -215,20 +193,20 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:20 - | - 2 | class GenericClass[T]: - | ^ - 3 | def __init__(self, value: T): - 4 | self.value = value - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:31 | 2 | class GenericClass[T]: 3 | def __init__(self, value: T): - | ^ + | ^ Clicking here + 4 | self.value = value + | + info: Found 1 declaration + --> main.py:2:20 + | + 2 | class GenericClass[T]: + | - + 3 | def __init__(self, value: T): 4 | self.value = value | "); @@ -247,23 +225,23 @@ mod tests { ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:2:1 - | - 2 | x = "outer" - | ^ - 3 | def outer_func(): - 4 | def inner_func(): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:16 | 3 | def outer_func(): 4 | def inner_func(): 5 | return x # Should find outer x - | ^ + | ^ Clicking here 6 | return inner_func | + info: Found 1 declaration + --> main.py:2:1 + | + 2 | x = "outer" + | - + 3 | def outer_func(): + 4 | def inner_func(): + | "#); } @@ -307,20 +285,20 @@ variable = 42 .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> mymodule.py:1:1 - | - 1 | - | ^ - 2 | def function(): - 3 | return "hello from mymodule" - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:7 | 2 | import mymodule 3 | print(mymodule.function()) - | ^^^^^^^^ + | ^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> mymodule.py:1:1 + | + 1 | + | - + 2 | def function(): + 3 | return "hello from mymodule" | "#); } @@ -348,19 +326,19 @@ def other_function(): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> mymodule.py:2:5 - | - 2 | def my_function(): - | ^^^^^^^^^^^ - 3 | return "hello" - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:7 | 2 | from mymodule import my_function 3 | print(my_function()) - | ^^^^^^^^^^^ + | ^^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> mymodule.py:2:5 + | + 2 | def my_function(): + | ----------- + 3 | return "hello" | "#); } @@ -390,22 +368,22 @@ FOO = 0 .build(); // Should find the submodule file itself - assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> mymodule/submodule.py:1:1 - | - 1 | - | ^ - 2 | FOO = 0 - | - info: Source + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Go to declaration --> main.py:3:7 | 2 | import mymodule.submodule as sub 3 | print(sub.helper()) - | ^^^ + | ^^^ Clicking here | - "#); + info: Found 1 declaration + --> mymodule/submodule.py:1:1 + | + 1 | + | - + 2 | FOO = 0 + | + "); } #[test] @@ -429,19 +407,19 @@ def func(arg): // Should resolve to the actual function definition, not the import statement assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> utils.py:2:5 - | - 2 | def func(arg): - | ^^^^ - 3 | return f"Processed: {arg}" - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:7 | 2 | from utils import func as h 3 | print(h("test")) - | ^ + | ^ Clicking here + | + info: Found 1 declaration + --> utils.py:2:5 + | + 2 | def func(arg): + | ---- + 3 | return f"Processed: {arg}" | "#); } @@ -473,19 +451,19 @@ def shared_function(): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> original.py:2:5 - | - 2 | def shared_function(): - | ^^^^^^^^^^^^^^^ - 3 | return "from original" - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:7 | 2 | from intermediate import shared_function 3 | print(shared_function()) - | ^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> original.py:2:5 + | + 2 | def shared_function(): + | --------------- + 3 | return "from original" | "#); } @@ -515,20 +493,20 @@ def multiply_numbers(a, b): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> math_utils.py:2:5 - | - 2 | def add_numbers(a, b): - | ^^^^^^^^^^^ - 3 | """Add two numbers together.""" - 4 | return a + b - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:10 | 2 | from math_utils import * 3 | result = add_numbers(5, 3) - | ^^^^^^^^^^^ + | ^^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> math_utils.py:2:5 + | + 2 | def add_numbers(a, b): + | ----------- + 3 | """Add two numbers together.""" + 4 | return a + b | "#); } @@ -565,20 +543,20 @@ def another_helper(): // Should resolve the relative import to find the actual function definition assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> package/utils.py:2:5 - | - 2 | def helper_function(arg): - | ^^^^^^^^^^^^^^^ - 3 | """A helper function in utils module.""" - 4 | return f"Processed: {arg}" - | - info: Source + info[goto-declaration]: Go to declaration --> package/main.py:3:10 | 2 | from .utils import helper_function 3 | result = helper_function("test") - | ^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> package/utils.py:2:5 + | + 2 | def helper_function(arg): + | --------------- + 3 | """A helper function in utils module.""" + 4 | return f"Processed: {arg}" | "#); } @@ -614,20 +592,20 @@ def another_helper(): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> package/utils.py:2:5 - | - 2 | def helper_function(arg): - | ^^^^^^^^^^^^^^^ - 3 | """A helper function in utils module.""" - 4 | return f"Processed: {arg}" - | - info: Source + info[goto-declaration]: Go to declaration --> package/main.py:3:10 | 2 | from .utils import * 3 | result = helper_function("test") - | ^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> package/utils.py:2:5 + | + 2 | def helper_function(arg): + | --------------- + 3 | """A helper function in utils module.""" + 4 | return f"Processed: {arg}" | "#); } @@ -657,20 +635,20 @@ FOO = 0 .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mymodule/submodule.py:1:1 - | - 1 | - | ^ - 2 | FOO = 0 - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:30 | 2 | import mymodule.submodule as sub - | ^^^ + | ^^^ Clicking here 3 | print(sub.helper()) | + info: Found 1 declaration + --> mymodule/submodule.py:1:1 + | + 1 | + | - + 2 | FOO = 0 + | "); } @@ -699,20 +677,20 @@ FOO = 0 .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mymodule/submodule.py:1:1 - | - 1 | - | ^ - 2 | FOO = 0 - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:17 | 2 | import mymodule.submodule as sub - | ^^^^^^^^^ + | ^^^^^^^^^ Clicking here 3 | print(sub.helper()) | + info: Found 1 declaration + --> mymodule/submodule.py:1:1 + | + 1 | + | - + 2 | FOO = 0 + | "); } @@ -745,20 +723,20 @@ def another_helper(path): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> mypackage/utils.py:2:5 - | - 2 | def helper(a, b): - | ^^^^^^ - 3 | return a + "/" + b - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:29 | 2 | from mypackage.utils import helper as h - | ^^^^^^ + | ^^^^^^ Clicking here 3 | result = h("/a", "/b") | + info: Found 1 declaration + --> mypackage/utils.py:2:5 + | + 2 | def helper(a, b): + | ------ + 3 | return a + "/" + b + | "#); } @@ -791,20 +769,20 @@ def another_helper(path): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> mypackage/utils.py:2:5 - | - 2 | def helper(a, b): - | ^^^^^^ - 3 | return a + "/" + b - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:39 | 2 | from mypackage.utils import helper as h - | ^ + | ^ Clicking here 3 | result = h("/a", "/b") | + info: Found 1 declaration + --> mypackage/utils.py:2:5 + | + 2 | def helper(a, b): + | ------ + 3 | return a + "/" + b + | "#); } @@ -837,21 +815,21 @@ def another_helper(path): .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> mypackage/utils.py:1:1 - | - 1 | - | ^ - 2 | def helper(a, b): - 3 | return a + "/" + b - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:16 | 2 | from mypackage.utils import helper as h - | ^^^^^ + | ^^^^^ Clicking here 3 | result = h("/a", "/b") | + info: Found 1 declaration + --> mypackage/utils.py:1:1 + | + 1 | + | - + 2 | def helper(a, b): + 3 | return a + "/" + b + | "#); } @@ -869,23 +847,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:7:7 + | + 6 | c = C() + 7 | y = c.x + | ^ Clicking here + | + info: Found 1 declaration --> main.py:4:9 | 2 | class C: 3 | def __init__(self): 4 | self.x: int = 1 - | ^^^^^^ + | ------ 5 | 6 | c = C() | - info: Source - --> main.py:7:7 - | - 6 | c = C() - 7 | y = c.x - | ^ - | "); } @@ -901,23 +879,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:2:5 + | + 2 | a: "MyClass" = 1 + | ^^^^^^^ Clicking here + 3 | + 4 | class MyClass: + | + info: Found 1 declaration --> main.py:4:7 | 2 | a: "MyClass" = 1 3 | 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | - info: Source - --> main.py:2:5 - | - 2 | a: "MyClass" = 1 - | ^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -933,23 +911,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:2:12 + | + 2 | a: "None | MyClass" = 1 + | ^^^^^^^ Clicking here + 3 | + 4 | class MyClass: + | + info: Found 1 declaration --> main.py:4:7 | 2 | a: "None | MyClass" = 1 3 | 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | - info: Source - --> main.py:2:12 - | - 2 | a: "None | MyClass" = 1 - | ^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -979,23 +957,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:2:12 + | + 2 | a: "None | MyClass" = 1 + | ^^^^^^^ Clicking here + 3 | + 4 | class MyClass: + | + info: Found 1 declaration --> main.py:4:7 | 2 | a: "None | MyClass" = 1 3 | 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | - info: Source - --> main.py:2:12 - | - 2 | a: "None | MyClass" = 1 - | ^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -1039,23 +1017,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:2:5 + | + 2 | a: "MyClass | No" = 1 + | ^^^^^^^ Clicking here + 3 | + 4 | class MyClass: + | + info: Found 1 declaration --> main.py:4:7 | 2 | a: "MyClass | No" = 1 3 | 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | - info: Source - --> main.py:2:5 - | - 2 | a: "MyClass | No" = 1 - | ^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -1082,17 +1060,17 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:2:1 - | - 2 | ab: "ab" - | ^^ - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:6 | 2 | ab: "ab" - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:2:1 + | + 2 | ab: "ab" + | -- | "#); } @@ -1126,23 +1104,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:11:9 + | + 10 | d = D() + 11 | y = d.y.x + | ^ Clicking here + | + info: Found 1 declaration --> main.py:4:9 | 2 | class C: 3 | def __init__(self): 4 | self.x: int = 1 - | ^^^^^^ + | ------ 5 | 6 | class D: | - info: Source - --> main.py:11:9 - | - 10 | d = D() - 11 | y = d.y.x - | ^ - | "); } @@ -1160,23 +1138,23 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:7:7 + | + 6 | c = C() + 7 | y = c.x + | ^ Clicking here + | + info: Found 1 declaration --> main.py:4:9 | 2 | class C: 3 | def __init__(self): 4 | self.x = 1 - | ^^^^^^ + | ------ 5 | 6 | c = C() | - info: Source - --> main.py:7:7 - | - 6 | c = C() - 7 | y = c.x - | ^ - | "); } @@ -1194,20 +1172,20 @@ def another_helper(path): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:3:9 - | - 2 | class C: - 3 | def foo(self): - | ^^^ - 4 | return 42 - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:7:9 | 6 | c = C() 7 | res = c.foo() - | ^^^ + | ^^^ Clicking here + | + info: Found 1 declaration + --> main.py:3:9 + | + 2 | class C: + 3 | def foo(self): + | --- + 4 | return 42 | "); } @@ -1244,7 +1222,7 @@ x: int = 42 "Should find the int class definition" ); assert!( - result.contains("info[goto-declaration]: Declaration"), + result.contains("info[goto-declaration]: Go to declaration"), "Should be a goto-declaration result" ); } @@ -1267,25 +1245,25 @@ def outer(): // Should find the variable declaration in the outer scope, not the nonlocal statement assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:3:5 - | - 2 | def outer(): - 3 | x = "outer_value" - | ^ - 4 | - 5 | def inner(): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:8:16 | 6 | nonlocal x 7 | x = "modified" 8 | return x # Should find the nonlocal x declaration in outer scope - | ^ + | ^ Clicking here 9 | 10 | return inner | + info: Found 1 declaration + --> main.py:3:5 + | + 2 | def outer(): + 3 | x = "outer_value" + | - + 4 | + 5 | def inner(): + | "#); } @@ -1307,24 +1285,24 @@ def outer(): // Should find the variable declaration in the outer scope, not the nonlocal statement assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:3:5 - | - 2 | def outer(): - 3 | xy = "outer_value" - | ^^ - 4 | - 5 | def inner(): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:6:18 | 5 | def inner(): 6 | nonlocal xy - | ^^ + | ^^ Clicking here 7 | xy = "modified" 8 | return x # Should find the nonlocal x declaration in outer scope | + info: Found 1 declaration + --> main.py:3:5 + | + 2 | def outer(): + 3 | xy = "outer_value" + | -- + 4 | + 5 | def inner(): + | "#); } @@ -1343,21 +1321,21 @@ def function(): // Should find the global variable declaration, not the global statement assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:2:1 - | - 2 | global_var = "global_value" - | ^^^^^^^^^^ - 3 | - 4 | def function(): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:7:12 | 5 | global global_var 6 | global_var = "modified" 7 | return global_var # Should find the global variable declaration - | ^^^^^^^^^^ + | ^^^^^^^^^^ Clicking here + | + info: Found 1 declaration + --> main.py:2:1 + | + 2 | global_var = "global_value" + | ---------- + 3 | + 4 | def function(): | "#); } @@ -1377,23 +1355,23 @@ def function(): // Should find the global variable declaration, not the global statement assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:2:1 - | - 2 | global_var = "global_value" - | ^^^^^^^^^^ - 3 | - 4 | def function(): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:12 | 4 | def function(): 5 | global global_var - | ^^^^^^^^^^ + | ^^^^^^^^^^ Clicking here 6 | global_var = "modified" 7 | return global_var # Should find the global variable declaration | + info: Found 1 declaration + --> main.py:2:1 + | + 2 | global_var = "global_value" + | ---------- + 3 | + 4 | def function(): + | "#); } @@ -1413,21 +1391,21 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:3:5 - | - 2 | class A: - 3 | x = 10 - | ^ - 4 | - 5 | class B(A): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:9:7 | 8 | b = B() 9 | y = b.x - | ^ + | ^ Clicking here + | + info: Found 1 declaration + --> main.py:3:5 + | + 2 | class A: + 3 | x = 10 + | - + 4 | + 5 | class B(A): | "); } @@ -1444,22 +1422,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:4:22 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ab]: - | ^^ + | ^^ Clicking here 5 | x = ab | - info: Source + info: Found 1 declaration --> main.py:4:22 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ab]: - | ^^ + | -- 5 | x = ab | "#); @@ -1477,22 +1455,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:4:22 - | - 2 | def my_func(command: str): - 3 | match command.split(): - 4 | case ["get", ab]: - | ^^ - 5 | x = ab - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:17 | 3 | match command.split(): 4 | case ["get", ab]: 5 | x = ab - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:4:22 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ab]: + | -- + 5 | x = ab | "#); } @@ -1509,22 +1487,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:4:23 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", *ab]: - | ^^ + | ^^ Clicking here 5 | x = ab | - info: Source + info: Found 1 declaration --> main.py:4:23 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", *ab]: - | ^^ + | -- 5 | x = ab | "#); @@ -1542,22 +1520,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:4:23 - | - 2 | def my_func(command: str): - 3 | match command.split(): - 4 | case ["get", *ab]: - | ^^ - 5 | x = ab - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:17 | 3 | match command.split(): 4 | case ["get", *ab]: 5 | x = ab - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:4:23 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", *ab]: + | -- + 5 | x = ab | "#); } @@ -1574,22 +1552,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:4:37 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ("a" | "b") as ab]: - | ^^ + | ^^ Clicking here 5 | x = ab | - info: Source + info: Found 1 declaration --> main.py:4:37 | 2 | def my_func(command: str): 3 | match command.split(): 4 | case ["get", ("a" | "b") as ab]: - | ^^ + | -- 5 | x = ab | "#); @@ -1607,22 +1585,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:4:37 - | - 2 | def my_func(command: str): - 3 | match command.split(): - 4 | case ["get", ("a" | "b") as ab]: - | ^^ - 5 | x = ab - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:17 | 3 | match command.split(): 4 | case ["get", ("a" | "b") as ab]: 5 | x = ab - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:4:37 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ("a" | "b") as ab]: + | -- + 5 | x = ab | "#); } @@ -1645,22 +1623,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:10:30 | 8 | def my_func(event: Click): 9 | match event: 10 | case Click(x, button=ab): - | ^^ + | ^^ Clicking here 11 | x = ab | - info: Source + info: Found 1 declaration --> main.py:10:30 | 8 | def my_func(event: Click): 9 | match event: 10 | case Click(x, button=ab): - | ^^ + | -- 11 | x = ab | "); @@ -1684,22 +1662,22 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:10:30 - | - 8 | def my_func(event: Click): - 9 | match event: - 10 | case Click(x, button=ab): - | ^^ - 11 | x = ab - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:11:17 | 9 | match event: 10 | case Click(x, button=ab): 11 | x = ab - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:10:30 + | + 8 | def my_func(event: Click): + 9 | match event: + 10 | case Click(x, button=ab): + | -- + 11 | x = ab | "); } @@ -1722,23 +1700,23 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:2:7 - | - 2 | class Click: - | ^^^^^ - 3 | __match_args__ = ("position", "button") - 4 | def __init__(self, pos, btn): - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:10:14 | 8 | def my_func(event: Click): 9 | match event: 10 | case Click(x, button=ab): - | ^^^^^ + | ^^^^^ Clicking here 11 | x = ab | + info: Found 1 declaration + --> main.py:2:7 + | + 2 | class Click: + | ----- + 3 | __match_args__ = ("position", "button") + 4 | def __init__(self, pos, btn): + | "#); } @@ -1771,17 +1749,17 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | ^^ Clicking here | - info: Source + info: Found 1 declaration --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | -- | "); } @@ -1795,17 +1773,17 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:13 - | - 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:37 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | -- | "); } @@ -1820,19 +1798,19 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:3:15 | 2 | from typing import Callable 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ + | ^^ Clicking here | - info: Source + info: Found 1 declaration --> main.py:3:15 | 2 | from typing import Callable 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ + | -- | "); } @@ -1847,19 +1825,19 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:3:15 - | - 2 | from typing import Callable - 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:3:43 | 2 | from typing import Callable 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:3:15 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | -- | "); } @@ -1873,17 +1851,17 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ + | ^^ Clicking here | - info: Source + info: Found 1 declaration --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ + | -- | "); } @@ -1897,17 +1875,17 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:14 - | - 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:2:38 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] - | ^^ + | ^^ Clicking here + | + info: Found 1 declaration + --> main.py:2:14 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | -- | "); } @@ -1930,21 +1908,21 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:7:9 - | - 6 | @property - 7 | def value(self): - | ^^^^^ - 8 | return self._value - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:11:3 | 10 | c = C() 11 | c.value = 42 - | ^^^^^ + | ^^^^^ Clicking here | + info: Found 1 declaration + --> main.py:7:9 + | + 6 | @property + 7 | def value(self): + | ----- + 8 | return self._value + | "); } @@ -1982,7 +1960,7 @@ def function(): "Should find the __doc__ attribute definition" ); assert!( - result.contains("info[goto-declaration]: Declaration"), + result.contains("info[goto-declaration]: Go to declaration"), "Should be a goto-declaration result" ); } @@ -2003,23 +1981,23 @@ def function(): ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:9:9 + | + 8 | def use_drawable(obj: Drawable): + 9 | obj.name + | ^^^^ Clicking here + | + info: Found 1 declaration --> main.py:6:5 | 4 | class Drawable(Protocol): 5 | def draw(self) -> None: ... 6 | name: str - | ^^^^ + | ---- 7 | 8 | def use_drawable(obj: Drawable): | - info: Source - --> main.py:9:9 - | - 8 | def use_drawable(obj: Drawable): - 9 | obj.name - | ^^^^ - | "); } @@ -2037,24 +2015,24 @@ class MyClass: // Should find the ClassType defined in the class body, not fail to resolve assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:3:5 - | - 2 | class MyClass: - 3 | ClassType = int - | ^^^^^^^^^ - 4 | - 5 | def generic_method[T](self, value: ClassType) -> T: - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:40 | 3 | ClassType = int 4 | 5 | def generic_method[T](self, value: ClassType) -> T: - | ^^^^^^^^^ + | ^^^^^^^^^ Clicking here 6 | return value | + info: Found 1 declaration + --> main.py:3:5 + | + 2 | class MyClass: + 3 | ClassType = int + | --------- + 4 | + 5 | def generic_method[T](self, value: ClassType) -> T: + | "); } @@ -2070,20 +2048,20 @@ class MyClass: ); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> main.py:2:20 - | - 2 | def my_function(x, y, z=10): - | ^ - 3 | return x + y + z - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:5:25 | 3 | return x + y + z 4 | 5 | result = my_function(1, y=2, z=3) - | ^ + | ^ Clicking here + | + info: Found 1 declaration + --> main.py:2:20 + | + 2 | def my_function(x, y, z=10): + | - + 3 | return x + y + z | "); } @@ -2110,39 +2088,26 @@ class MyClass: // Should navigate to the parameter in both matching overloads assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration - --> main.py:5:24 - | - 4 | @overload - 5 | def process(data: str, format: str) -> str: ... - | ^^^^^^ - 6 | - 7 | @overload - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:14:27 | 13 | # Call the overloaded function 14 | result = process("hello", format="json") - | ^^^^^^ + | ^^^^^^ Clicking here | - - info[goto-declaration]: Declaration - --> main.py:8:24 + info: Found 2 declarations + --> main.py:5:24 | + 4 | @overload + 5 | def process(data: str, format: str) -> str: ... + | ------ + 6 | 7 | @overload 8 | def process(data: int, format: int) -> int: ... - | ^^^^^^ + | ------ 9 | 10 | def process(data, format): | - info: Source - --> main.py:14:27 - | - 13 | # Call the overloaded function - 14 | result = process("hello", format="json") - | ^^^^^^ - | "#); } @@ -2179,38 +2144,24 @@ def ab(a: str): ... .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:4:1 + | + 2 | from mymodule import ab + 3 | + 4 | ab(1) + | ^^ Clicking here + | + info: Found 2 declarations --> mymodule.pyi:5:5 | 4 | @overload 5 | def ab(a: int): ... - | ^^ + | -- 6 | - 7 | @overload - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1) - | ^^ - | - - info[goto-declaration]: Declaration - --> mymodule.pyi:8:5 - | 7 | @overload 8 | def ab(a: str): ... - | ^^ - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1) - | ^^ + | -- | "); } @@ -2248,38 +2199,24 @@ def ab(a: str): ... .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:4:1 + | + 2 | from mymodule import ab + 3 | + 4 | ab("hello") + | ^^ Clicking here + | + info: Found 2 declarations --> mymodule.pyi:5:5 | 4 | @overload 5 | def ab(a: int): ... - | ^^ + | -- 6 | - 7 | @overload - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab("hello") - | ^^ - | - - info[goto-declaration]: Declaration - --> mymodule.pyi:8:5 - | 7 | @overload 8 | def ab(a: str): ... - | ^^ - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab("hello") - | ^^ + | -- | "#); } @@ -2317,38 +2254,24 @@ def ab(a: int): ... .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:4:1 + | + 2 | from mymodule import ab + 3 | + 4 | ab(1, 2) + | ^^ Clicking here + | + info: Found 2 declarations --> mymodule.pyi:5:5 | 4 | @overload 5 | def ab(a: int, b: int): ... - | ^^ + | -- 6 | - 7 | @overload - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1, 2) - | ^^ - | - - info[goto-declaration]: Declaration - --> mymodule.pyi:8:5 - | 7 | @overload 8 | def ab(a: int): ... - | ^^ - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1, 2) - | ^^ + | -- | "); } @@ -2386,38 +2309,24 @@ def ab(a: int): ... .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:4:1 + | + 2 | from mymodule import ab + 3 | + 4 | ab(1) + | ^^ Clicking here + | + info: Found 2 declarations --> mymodule.pyi:5:5 | 4 | @overload 5 | def ab(a: int, b: int): ... - | ^^ + | -- 6 | - 7 | @overload - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1) - | ^^ - | - - info[goto-declaration]: Declaration - --> mymodule.pyi:8:5 - | 7 | @overload 8 | def ab(a: int): ... - | ^^ - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1) - | ^^ + | -- | "); } @@ -2458,57 +2367,29 @@ def ab(a: int, *, c: int): ... .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mymodule.pyi:5:5 - | - 4 | @overload - 5 | def ab(a: int): ... - | ^^ - 6 | - 7 | @overload - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1, b=2) - | ^^ + | ^^ Clicking here | - - info[goto-declaration]: Declaration - --> mymodule.pyi:8:5 + info: Found 3 declarations + --> mymodule.pyi:5:5 | + 4 | @overload + 5 | def ab(a: int): ... + | -- + 6 | 7 | @overload 8 | def ab(a: int, *, b: int): ... - | ^^ + | -- 9 | - 10 | @overload - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1, b=2) - | ^^ - | - - info[goto-declaration]: Declaration - --> mymodule.pyi:11:5 - | 10 | @overload 11 | def ab(a: int, *, c: int): ... - | ^^ + | -- | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1, b=2) - | ^^ - | "); } @@ -2548,57 +2429,29 @@ def ab(a: int, *, c: int): ... .build(); assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mymodule.pyi:5:5 - | - 4 | @overload - 5 | def ab(a: int): ... - | ^^ - 6 | - 7 | @overload - | - info: Source + info[goto-declaration]: Go to declaration --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1, c=2) - | ^^ + | ^^ Clicking here | - - info[goto-declaration]: Declaration - --> mymodule.pyi:8:5 + info: Found 3 declarations + --> mymodule.pyi:5:5 | + 4 | @overload + 5 | def ab(a: int): ... + | -- + 6 | 7 | @overload 8 | def ab(a: int, *, b: int): ... - | ^^ + | -- 9 | - 10 | @overload - | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1, c=2) - | ^^ - | - - info[goto-declaration]: Declaration - --> mymodule.pyi:11:5 - | 10 | @overload 11 | def ab(a: int, *, c: int): ... - | ^^ + | -- | - info: Source - --> main.py:4:1 - | - 2 | from mymodule import ab - 3 | - 4 | ab(1, c=2) - | ^^ - | "); } @@ -2627,21 +2480,21 @@ def ab(a: int, *, c: int): ... // which is correct but unhelpful. Unfortunately even if it only claimed the LHS identifier it // would highlight `subpkg.submod` which is strictly better but still isn't what we want. assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mypackage/__init__.py:2:1 - | - 2 | from .subpkg.submod import val - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 3 | - 4 | x = subpkg - | - info: Source + info[goto-declaration]: Go to declaration --> mypackage/__init__.py:4:5 | 2 | from .subpkg.submod import val 3 | 4 | x = subpkg - | ^^^^^^ + | ^^^^^^ Clicking here + | + info: Found 1 declaration + --> mypackage/__init__.py:2:1 + | + 2 | from .subpkg.submod import val + | ------------------------------ + 3 | + 4 | x = subpkg | "); } @@ -2671,18 +2524,18 @@ def ab(a: int, *, c: int): ... // `subpkg = mypackage.subpkg`. As in, it's both defining a local `subpkg` and // loading the module `mypackage.subpkg`, so, it's understandable to get confused! assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mypackage/subpkg/__init__.py:1:1 - | - | - info: Source + info[goto-declaration]: Go to declaration --> mypackage/__init__.py:2:7 | 2 | from .subpkg.submod import val - | ^^^^^^ + | ^^^^^^ Clicking here 3 | 4 | x = subpkg | + info: Found 1 declaration + --> mypackage/subpkg/__init__.py:1:1 + | + | "); } @@ -2732,21 +2585,21 @@ def ab(a: int, *, c: int): ... // Going to the submod module is correct! assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mypackage/subpkg/submod.py:1:1 - | - 1 | - | ^ - 2 | val: int = 0 - | - info: Source + info[goto-declaration]: Go to declaration --> mypackage/__init__.py:2:14 | 2 | from .subpkg.submod import val - | ^^^^^^ + | ^^^^^^ Clicking here 3 | 4 | x = submod | + info: Found 1 declaration + --> mypackage/subpkg/submod.py:1:1 + | + 1 | + | - + 2 | val: int = 0 + | "); } @@ -2771,21 +2624,21 @@ def ab(a: int, *, c: int): ... // Going to the subpkg module is correct! assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mypackage/subpkg/__init__.py:1:1 - | - 1 | - | ^ - 2 | subpkg: int = 10 - | - info: Source + info[goto-declaration]: Go to declaration --> mypackage/__init__.py:2:7 | 2 | from .subpkg import subpkg - | ^^^^^^ + | ^^^^^^ Clicking here 3 | 4 | x = subpkg | + info: Found 1 declaration + --> mypackage/subpkg/__init__.py:1:1 + | + 1 | + | - + 2 | subpkg: int = 10 + | "); } @@ -2810,20 +2663,20 @@ def ab(a: int, *, c: int): ... // Going to the subpkg `int` is correct! assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration - --> mypackage/subpkg/__init__.py:2:1 - | - 2 | subpkg: int = 10 - | ^^^^^^ - | - info: Source + info[goto-declaration]: Go to declaration --> mypackage/__init__.py:2:21 | 2 | from .subpkg import subpkg - | ^^^^^^ + | ^^^^^^ Clicking here 3 | 4 | x = subpkg | + info: Found 1 declaration + --> mypackage/subpkg/__init__.py:2:1 + | + 2 | subpkg: int = 10 + | ------ + | "); } @@ -2860,36 +2713,26 @@ def ab(a: int, *, c: int): ... // that *immediately* overwrites the `ImportFromSubmodule`'s definition // This span seemingly doesn't appear at all!? Is it getting hidden by the LHS span? assert_snapshot!(test.goto_declaration(), @r" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> mypackage/__init__.py:4:5 + | + 2 | from .subpkg import subpkg + 3 | + 4 | x = subpkg + | ^^^^^^ Clicking here + | + info: Found 2 declarations --> mypackage/__init__.py:2:1 | 2 | from .subpkg import subpkg - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | -------------------------- 3 | 4 | x = subpkg | - info: Source - --> mypackage/__init__.py:4:5 - | - 2 | from .subpkg import subpkg - 3 | - 4 | x = subpkg - | ^^^^^^ - | - - info[goto-declaration]: Declaration - --> mypackage/subpkg/__init__.py:2:1 + ::: mypackage/subpkg/__init__.py:2:1 | 2 | subpkg: int = 10 - | ^^^^^^ - | - info: Source - --> mypackage/__init__.py:4:5 - | - 2 | from .subpkg import subpkg - 3 | - 4 | x = subpkg - | ^^^^^^ + | ------ | "); } @@ -2913,71 +2756,38 @@ def ab(a: int, *, c: int): ... .build(); assert_snapshot!(test.goto_declaration(), @r#" - info[goto-declaration]: Declaration + info[goto-declaration]: Go to declaration + --> main.py:6:7 + | + 4 | a: int = 10 + 5 | + 6 | print(a) + | ^ Clicking here + 7 | + 8 | a: bool = True + | + info: Found 3 declarations --> main.py:2:1 | 2 | a: str = "test" - | ^ + | - 3 | 4 | a: int = 10 - | - info: Source - --> main.py:6:7 - | - 4 | a: int = 10 + | - 5 | - 6 | print(a) - | ^ - 7 | - 8 | a: bool = True - | - - info[goto-declaration]: Declaration - --> main.py:4:1 - | - 2 | a: str = "test" - 3 | - 4 | a: int = 10 - | ^ - 5 | - 6 | print(a) - | - info: Source - --> main.py:6:7 - | - 4 | a: int = 10 - 5 | - 6 | print(a) - | ^ - 7 | - 8 | a: bool = True - | - - info[goto-declaration]: Declaration - --> main.py:8:1 - | 6 | print(a) 7 | 8 | a: bool = True - | ^ - | - info: Source - --> main.py:6:7 - | - 4 | a: int = 10 - 5 | - 6 | print(a) - | ^ - 7 | - 8 | a: bool = True + | - | "#); } impl CursorTest { fn goto_declaration(&self) -> String { - let Some(targets) = goto_declaration(&self.db, self.cursor.file, self.cursor.offset) - else { + let Some(targets) = salsa::attach(&self.db, || { + goto_declaration(&self.db, self.cursor.file, self.cursor.offset) + }) else { return "No goto target found".to_string(); }; @@ -2985,47 +2795,10 @@ def ab(a: int, *, c: int): ... return "No declarations found".to_string(); } - let source = targets.range; - self.render_diagnostics( - targets - .into_iter() - .map(|target| GotoDeclarationDiagnostic::new(source, &target)), - ) - } - } - - struct GotoDeclarationDiagnostic { - source: FileRange, - target: FileRange, - } - - impl GotoDeclarationDiagnostic { - fn new(source: FileRange, target: &NavigationTarget) -> Self { - Self { - source, - target: FileRange::new(target.file(), target.focus_range()), - } - } - } - - impl IntoDiagnostic for GotoDeclarationDiagnostic { - fn into_diagnostic(self) -> Diagnostic { - let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source"); - source.annotate(Annotation::primary( - Span::from(self.source.file()).with_range(self.source.range()), - )); - - let mut main = Diagnostic::new( - DiagnosticId::Lint(LintName::of("goto-declaration")), - Severity::Info, - "Declaration".to_string(), - ); - main.annotate(Annotation::primary( - Span::from(self.target.file()).with_range(self.target.range()), - )); - main.sub(source); - - main + self.render_diagnostics([crate::goto_definition::test::GotoDiagnostic::new( + crate::goto_definition::test::GotoAction::Declaration, + targets, + )]) } } } diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index 00e08957cb..e942e8040d 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -30,15 +30,15 @@ pub fn goto_definition( } #[cfg(test)] -mod test { +pub(super) mod test { + use crate::tests::{CursorTest, IntoDiagnostic}; - use crate::{NavigationTarget, goto_definition}; + use crate::{NavigationTargets, RangedValue, goto_definition}; use insta::assert_snapshot; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, SubDiagnosticSeverity, }; - use ruff_db::files::FileRange; use ruff_text_size::Ranged; /// goto-definition on a module should go to the .py not the .pyi @@ -70,19 +70,19 @@ def my_function(): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:1:1 - | - 1 | - | ^ - 2 | def my_function(): - 3 | return "hello" - | - info: Source + info[goto-definition]: Go to definition --> main.py:2:6 | 2 | from mymodule import my_function - | ^^^^^^^^ + | ^^^^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:1:1 + | + 1 | + | - + 2 | def my_function(): + 3 | return "hello" | "#); } @@ -114,20 +114,20 @@ def my_function(): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:1:1 - | - 1 | - | ^ - 2 | def my_function(): - 3 | return "hello" - | - info: Source + info[goto-definition]: Go to definition --> main.py:3:5 | 2 | import mymodule 3 | x = mymodule - | ^^^^^^^^ + | ^^^^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:1:1 + | + 1 | + | - + 2 | def my_function(): + 3 | return "hello" | "#); } @@ -164,19 +164,19 @@ def other_function(): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def my_function(): - | ^^^^^^^^^^^ - 3 | return "hello" - | - info: Source + info[goto-definition]: Go to definition --> main.py:3:7 | 2 | from mymodule import my_function 3 | print(my_function()) - | ^^^^^^^^^^^ + | ^^^^^^^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def my_function(): + | ----------- + 3 | return "hello" | "#); } @@ -206,21 +206,21 @@ def other_function(): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def my_function(): - | ^^^^^^^^^^^ - 3 | return "hello" - | - info: Source + info[goto-definition]: Go to definition --> mymodule.pyi:2:5 | 2 | def my_function(): ... - | ^^^^^^^^^^^ + | ^^^^^^^^^^^ Clicking here 3 | 4 | def other_function(): ... | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def my_function(): + | ----------- + 3 | return "hello" + | "#); } @@ -266,54 +266,28 @@ def other_function(): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition + info[goto-definition]: Go to definition + --> main.py:3:7 + | + 2 | from mymodule import my_function + 3 | print(my_function()) + | ^^^^^^^^^^^ Clicking here + | + info: Found 3 definitions --> mymodule.py:2:5 | 2 | def my_function(): - | ^^^^^^^^^^^ - 3 | return "hello" - | - info: Source - --> main.py:3:7 - | - 2 | from mymodule import my_function - 3 | print(my_function()) - | ^^^^^^^^^^^ - | - - info[goto-definition]: Definition - --> mymodule.py:5:5 - | + | ----------- 3 | return "hello" 4 | 5 | def my_function(): - | ^^^^^^^^^^^ - 6 | return "hello again" - | - info: Source - --> main.py:3:7 - | - 2 | from mymodule import my_function - 3 | print(my_function()) - | ^^^^^^^^^^^ - | - - info[goto-definition]: Definition - --> mymodule.py:8:5 - | + | ----------- 6 | return "hello again" 7 | 8 | def my_function(): - | ^^^^^^^^^^^ + | ----------- 9 | return "we can't keep doing this" | - info: Source - --> main.py:3:7 - | - 2 | from mymodule import my_function - 3 | print(my_function()) - | ^^^^^^^^^^^ - | "#); } @@ -353,20 +327,20 @@ class MyOtherClass: .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> mymodule.py:2:7 - | - 2 | class MyClass: - | ^^^^^^^ - 3 | def __init__(self, val): - 4 | self.val = val - | - info: Source + info[goto-definition]: Go to definition --> main.py:3:5 | 2 | from mymodule import MyClass 3 | x = MyClass - | ^^^^^^^ + | ^^^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:7 + | + 2 | class MyClass: + | ------- + 3 | def __init__(self, val): + 4 | self.val = val | "); } @@ -400,21 +374,21 @@ class MyOtherClass: .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> mymodule.py:2:7 - | - 2 | class MyClass: - | ^^^^^^^ - 3 | def __init__(self, val): - 4 | self.val = val - | - info: Source + info[goto-definition]: Go to definition --> mymodule.pyi:2:7 | 2 | class MyClass: - | ^^^^^^^ + | ^^^^^^^ Clicking here 3 | def __init__(self, val: bool): ... | + info: Found 1 definition + --> mymodule.py:2:7 + | + 2 | class MyClass: + | ------- + 3 | def __init__(self, val): + 4 | self.val = val + | "); } @@ -454,37 +428,22 @@ class MyOtherClass: .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition + info[goto-definition]: Go to definition + --> main.py:3:5 + | + 2 | from mymodule import MyClass + 3 | x = MyClass(0) + | ^^^^^^^ Clicking here + | + info: Found 2 definitions --> mymodule.py:2:7 | 2 | class MyClass: - | ^^^^^^^ + | ------- 3 | def __init__(self, val): + | -------- 4 | self.val = val | - info: Source - --> main.py:3:5 - | - 2 | from mymodule import MyClass - 3 | x = MyClass(0) - | ^^^^^^^ - | - - info[goto-definition]: Definition - --> mymodule.py:3:9 - | - 2 | class MyClass: - 3 | def __init__(self, val): - | ^^^^^^^^ - 4 | self.val = val - | - info: Source - --> main.py:3:5 - | - 2 | from mymodule import MyClass - 3 | x = MyClass(0) - | ^^^^^^^ - | "); } @@ -528,22 +487,22 @@ class MyOtherClass: .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> mymodule.py:5:9 - | - 3 | def __init__(self, val): - 4 | self.val = val - 5 | def action(self): - | ^^^^^^ - 6 | print(self.val) - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:3 | 2 | from mymodule import MyClass 3 | x = MyClass(0) 4 | x.action() - | ^^^^^^ + | ^^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:5:9 + | + 3 | def __init__(self, val): + 4 | self.val = val + 5 | def action(self): + | ------ + 6 | print(self.val) | "); } @@ -587,22 +546,22 @@ class MyOtherClass: .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition + info[goto-definition]: Go to definition + --> main.py:3:13 + | + 2 | from mymodule import MyClass + 3 | x = MyClass.action() + | ^^^^^^ Clicking here + | + info: Found 1 definition --> mymodule.py:5:9 | 3 | def __init__(self, val): 4 | self.val = val 5 | def action(): - | ^^^^^^ + | ------ 6 | print("hi!") | - info: Source - --> main.py:3:13 - | - 2 | from mymodule import MyClass - 3 | x = MyClass.action() - | ^^^^^^ - | "#); } @@ -631,17 +590,17 @@ class MyClass: ... .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> mymodule.py:2:7 - | - 2 | class MyClass: ... - | ^^^^^^^ - | - info: Source + info[goto-definition]: Go to definition --> main.py:2:22 | 2 | from mymodule import MyClass - | ^^^^^^^ + | ^^^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:7 + | + 2 | class MyClass: ... + | ------- | "); } @@ -665,22 +624,22 @@ my_func(my_other_func(ab=5, y=2), 0) .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:2:13 - | - 2 | def my_func(ab, y, z = None): ... - | ^^ - 3 | def my_other_func(ab, y): ... - | - info: Source + info[goto-definition]: Go to definition --> main.py:5:23 | 3 | def my_other_func(ab, y): ... 4 | 5 | my_other_func(my_func(ab=5, y=2), 0) - | ^^ + | ^^ Clicking here 6 | my_func(my_other_func(ab=5, y=2), 0) | + info: Found 1 definition + --> main.py:2:13 + | + 2 | def my_func(ab, y, z = None): ... + | -- + 3 | def my_other_func(ab, y): ... + | "); } @@ -703,21 +662,21 @@ my_func(my_other_func(ab=5, y=2), 0) .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:19 - | - 2 | def my_func(ab, y, z = None): ... - 3 | def my_other_func(ab, y): ... - | ^^ - 4 | - 5 | my_other_func(my_func(ab=5, y=2), 0) - | - info: Source + info[goto-definition]: Go to definition --> main.py:6:23 | 5 | my_other_func(my_func(ab=5, y=2), 0) 6 | my_func(my_other_func(ab=5, y=2), 0) - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> main.py:3:19 + | + 2 | def my_func(ab, y, z = None): ... + 3 | def my_other_func(ab, y): ... + | -- + 4 | + 5 | my_other_func(my_func(ab=5, y=2), 0) | "); } @@ -741,22 +700,22 @@ my_func(my_other_func(ab=5, y=2), 0) .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:2:13 - | - 2 | def my_func(ab, y): ... - | ^^ - 3 | def my_other_func(ab, y): ... - | - info: Source + info[goto-definition]: Go to definition --> main.py:5:23 | 3 | def my_other_func(ab, y): ... 4 | 5 | my_other_func(my_func(ab=5, y=2), 0) - | ^^ + | ^^ Clicking here 6 | my_func(my_other_func(ab=5, y=2), 0) | + info: Found 1 definition + --> main.py:2:13 + | + 2 | def my_func(ab, y): ... + | -- + 3 | def my_other_func(ab, y): ... + | "); } @@ -779,21 +738,21 @@ my_func(my_other_func(ab=5, y=2), 0) .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:19 - | - 2 | def my_func(ab, y): ... - 3 | def my_other_func(ab, y): ... - | ^^ - 4 | - 5 | my_other_func(my_func(ab=5, y=2), 0) - | - info: Source + info[goto-definition]: Go to definition --> main.py:6:23 | 5 | my_other_func(my_func(ab=5, y=2), 0) 6 | my_func(my_other_func(ab=5, y=2), 0) - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> main.py:3:19 + | + 2 | def my_func(ab, y): ... + 3 | def my_other_func(ab, y): ... + | -- + 4 | + 5 | my_other_func(my_func(ab=5, y=2), 0) | "); } @@ -831,20 +790,20 @@ def ab(a: str): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def ab(a): - | ^^ - 3 | """the real implementation!""" - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1) - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def ab(a): + | -- + 3 | """the real implementation!""" | "#); } @@ -882,20 +841,20 @@ def ab(a: str): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def ab(a): - | ^^ - 3 | """the real implementation!""" - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab("hello") - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def ab(a): + | -- + 3 | """the real implementation!""" | "#); } @@ -933,20 +892,20 @@ def ab(a: int): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def ab(a, b = None): - | ^^ - 3 | """the real implementation!""" - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1, 2) - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def ab(a, b = None): + | -- + 3 | """the real implementation!""" | "#); } @@ -984,20 +943,20 @@ def ab(a: int): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def ab(a, b = None): - | ^^ - 3 | """the real implementation!""" - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1) - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def ab(a, b = None): + | -- + 3 | """the real implementation!""" | "#); } @@ -1038,20 +997,20 @@ def ab(a: int, *, c: int): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def ab(a, *, b = None, c = None): - | ^^ - 3 | """the real implementation!""" - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1, b=2) - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def ab(a, *, b = None, c = None): + | -- + 3 | """the real implementation!""" | "#); } @@ -1092,20 +1051,20 @@ def ab(a: int, *, c: int): ... .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition - --> mymodule.py:2:5 - | - 2 | def ab(a, *, b = None, c = None): - | ^^ - 3 | """the real implementation!""" - | - info: Source + info[goto-definition]: Go to definition --> main.py:4:1 | 2 | from mymodule import ab 3 | 4 | ab(1, c=2) - | ^^ + | ^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:5 + | + 2 | def ab(a, *, b = None, c = None): + | -- + 3 | """the real implementation!""" | "#); } @@ -1130,22 +1089,22 @@ a + b .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __add__(self, other): - | ^^^^^^^ - 4 | return Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:10:3 | 8 | b = Test() 9 | 10 | a + b - | ^ + | ^ Clicking here | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __add__(self, other): + | ------- + 4 | return Test() + | "); } @@ -1167,21 +1126,21 @@ B() + A() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class A: - 3 | def __radd__(self, other) -> A: - | ^^^^^^^^ - 4 | return self - | - info: Source + info[goto-definition]: Go to definition --> main.py:8:5 | 6 | class B: ... 7 | 8 | B() + A() - | ^ + | ^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class A: + 3 | def __radd__(self, other) -> A: + | -------- + 4 | return self | "); } @@ -1206,22 +1165,22 @@ a+b .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __add__(self, other): - | ^^^^^^^ - 4 | return Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:10:2 | 8 | b = Test() 9 | 10 | a+b - | ^ + | ^ Clicking here | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __add__(self, other): + | ------- + 4 | return Test() + | "); } @@ -1245,22 +1204,22 @@ a+b .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:8:1 - | - 7 | a = Test() - 8 | b = Test() - | ^ - 9 | - 10 | a+b - | - info: Source + info[goto-definition]: Go to definition --> main.py:10:3 | 8 | b = Test() 9 | 10 | a+b - | ^ + | ^ Clicking here + | + info: Found 1 definition + --> main.py:8:1 + | + 7 | a = Test() + 8 | b = Test() + | - + 9 | + 10 | a+b | "); } @@ -1304,22 +1263,22 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __invert__(self) -> 'Test': ... - | ^^^^^^^^^^ - 4 | - 5 | a = Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:1 | 5 | a = Test() 6 | 7 | ~a - | ^ + | ^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __invert__(self) -> 'Test': ... + | ---------- + 4 | + 5 | a = Test() | "); } @@ -1342,22 +1301,22 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __invert__(self, extra_arg) -> 'Test': ... - | ^^^^^^^^^^ - 4 | - 5 | a = Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:1 | 5 | a = Test() 6 | 7 | ~a - | ^ + | ^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __invert__(self, extra_arg) -> 'Test': ... + | ---------- + 4 | + 5 | a = Test() | "); } @@ -1379,22 +1338,22 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __invert__(self) -> 'Test': ... - | ^^^^^^^^^^ - 4 | - 5 | a = Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:1 | 5 | a = Test() 6 | 7 | ~ a - | ^ + | ^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __invert__(self) -> 'Test': ... + | ---------- + 4 | + 5 | a = Test() | "); } @@ -1416,23 +1375,23 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:5:1 - | - 3 | def __invert__(self) -> 'Test': ... - 4 | - 5 | a = Test() - | ^ - 6 | - 7 | -a - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:2 | 5 | a = Test() 6 | 7 | -a - | ^ + | ^ Clicking here + | + info: Found 1 definition + --> main.py:5:1 + | + 3 | def __invert__(self) -> 'Test': ... + 4 | + 5 | a = Test() + | - + 6 | + 7 | -a | "); } @@ -1454,22 +1413,22 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __bool__(self) -> bool: ... - | ^^^^^^^^ - 4 | - 5 | a = Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:1 | 5 | a = Test() 6 | 7 | not a - | ^^^ + | ^^^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __bool__(self) -> bool: ... + | -------- + 4 | + 5 | a = Test() | "); } @@ -1491,22 +1450,22 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __len__(self) -> 42: ... - | ^^^^^^^ - 4 | - 5 | a = Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:1 | 5 | a = Test() 6 | 7 | not a - | ^^^ + | ^^^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __len__(self) -> 42: ... + | ------- + 4 | + 5 | a = Test() | "); } @@ -1532,21 +1491,21 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __bool__(self, extra_arg) -> bool: ... - | ^^^^^^^^ - 4 | def __len__(self) -> 42: ... - | - info: Source + info[goto-definition]: Go to definition --> main.py:8:1 | 6 | a = Test() 7 | 8 | not a - | ^^^ + | ^^^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __bool__(self, extra_arg) -> bool: ... + | -------- + 4 | def __len__(self) -> 42: ... | "); } @@ -1572,22 +1531,22 @@ a = Test() .build(); assert_snapshot!(test.goto_definition(), @r" - info[goto-definition]: Definition - --> main.py:3:9 - | - 2 | class Test: - 3 | def __len__(self, extra_arg) -> 42: ... - | ^^^^^^^ - 4 | - 5 | a = Test() - | - info: Source + info[goto-definition]: Go to definition --> main.py:7:1 | 5 | a = Test() 6 | 7 | not a - | ^^^ + | ^^^ Clicking here + | + info: Found 1 definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __len__(self, extra_arg) -> 42: ... + | ------- + 4 | + 5 | a = Test() | "); } @@ -1604,36 +1563,28 @@ a: float = 3.14 .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition + info[goto-definition]: Go to definition + --> main.py:2:4 + | + 2 | a: float = 3.14 + | ^^^^^ Clicking here + | + info: Found 2 definitions --> stdlib/builtins.pyi:348:7 | 347 | @disjoint_base 348 | class int: - | ^^^ + | --- 349 | """int([x]) -> integer 350 | int(x, base=10) -> integer | - info: Source - --> main.py:2:4 - | - 2 | a: float = 3.14 - | ^^^^^ - | - - info[goto-definition]: Definition - --> stdlib/builtins.pyi:661:7 + ::: stdlib/builtins.pyi:661:7 | 660 | @disjoint_base 661 | class float: - | ^^^^^ + | ----- 662 | """Convert a string or number to a floating-point number, if possible.""" | - info: Source - --> main.py:2:4 - | - 2 | a: float = 3.14 - | ^^^^^ - | "#); } @@ -1649,51 +1600,35 @@ a: complex = 3.14 .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition + info[goto-definition]: Go to definition + --> main.py:2:4 + | + 2 | a: complex = 3.14 + | ^^^^^^^ Clicking here + | + info: Found 3 definitions --> stdlib/builtins.pyi:348:7 | 347 | @disjoint_base 348 | class int: - | ^^^ + | --- 349 | """int([x]) -> integer 350 | int(x, base=10) -> integer | - info: Source - --> main.py:2:4 - | - 2 | a: complex = 3.14 - | ^^^^^^^ - | - - info[goto-definition]: Definition - --> stdlib/builtins.pyi:661:7 + ::: stdlib/builtins.pyi:661:7 | 660 | @disjoint_base 661 | class float: - | ^^^^^ + | ----- 662 | """Convert a string or number to a floating-point number, if possible.""" | - info: Source - --> main.py:2:4 - | - 2 | a: complex = 3.14 - | ^^^^^^^ - | - - info[goto-definition]: Definition - --> stdlib/builtins.pyi:822:7 + ::: stdlib/builtins.pyi:822:7 | 821 | @disjoint_base 822 | class complex: - | ^^^^^^^ + | ------- 823 | """Create a complex number from a string or numbers. | - info: Source - --> main.py:2:4 - | - 2 | a: complex = 3.14 - | ^^^^^^^ - | "#); } @@ -1733,71 +1668,38 @@ TracebackType .build(); assert_snapshot!(test.goto_definition(), @r#" - info[goto-definition]: Definition + info[goto-definition]: Go to definition + --> main.py:6:7 + | + 4 | a: int = 10 + 5 | + 6 | print(a) + | ^ Clicking here + 7 | + 8 | a: bool = True + | + info: Found 3 definitions --> main.py:2:1 | 2 | a: str = "test" - | ^ + | - 3 | 4 | a: int = 10 - | - info: Source - --> main.py:6:7 - | - 4 | a: int = 10 + | - 5 | - 6 | print(a) - | ^ - 7 | - 8 | a: bool = True - | - - info[goto-definition]: Definition - --> main.py:4:1 - | - 2 | a: str = "test" - 3 | - 4 | a: int = 10 - | ^ - 5 | - 6 | print(a) - | - info: Source - --> main.py:6:7 - | - 4 | a: int = 10 - 5 | - 6 | print(a) - | ^ - 7 | - 8 | a: bool = True - | - - info[goto-definition]: Definition - --> main.py:8:1 - | 6 | print(a) 7 | 8 | a: bool = True - | ^ - | - info: Source - --> main.py:6:7 - | - 4 | a: int = 10 - 5 | - 6 | print(a) - | ^ - 7 | - 8 | a: bool = True + | - | "#); } impl CursorTest { fn goto_definition(&self) -> String { - let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset) - else { + let Some(targets) = salsa::attach(&self.db, || { + goto_definition(&self.db, self.cursor.file, self.cursor.offset) + }) else { return "No goto target found".to_string(); }; @@ -1805,47 +1707,86 @@ TracebackType return "No definitions found".to_string(); } - let source = targets.range; - self.render_diagnostics( - targets - .into_iter() - .map(|target| GotoDefinitionDiagnostic::new(source, &target)), - ) + self.render_diagnostics([GotoDiagnostic::new(GotoAction::Definition, targets)]) } } - struct GotoDefinitionDiagnostic { - source: FileRange, - target: FileRange, + pub(crate) struct GotoDiagnostic { + action: GotoAction, + targets: RangedValue, } - impl GotoDefinitionDiagnostic { - fn new(source: FileRange, target: &NavigationTarget) -> Self { - Self { - source, - target: FileRange::new(target.file(), target.focus_range()), - } + impl GotoDiagnostic { + pub(crate) fn new(action: GotoAction, targets: RangedValue) -> Self { + Self { action, targets } } } - impl IntoDiagnostic for GotoDefinitionDiagnostic { + impl IntoDiagnostic for GotoDiagnostic { fn into_diagnostic(self) -> Diagnostic { - let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source"); - source.annotate(Annotation::primary( - Span::from(self.source.file()).with_range(self.source.range()), - )); - + let source = self.targets.range; let mut main = Diagnostic::new( - DiagnosticId::Lint(LintName::of("goto-definition")), + DiagnosticId::Lint(LintName::of(self.action.name())), Severity::Info, - "Definition".to_string(), + self.action.label().to_string(), ); - main.annotate(Annotation::primary( - Span::from(self.target.file()).with_range(self.target.range()), - )); - main.sub(source); + + main.annotate( + Annotation::primary(Span::from(source.file()).with_range(source.range())) + .message("Clicking here"), + ); + + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!( + "Found {} {}{}", + self.targets.len(), + self.action.item_label(), + if self.targets.len() == 1 { "" } else { "s" } + ), + ); + + for target in self.targets { + sub.annotate(Annotation::secondary( + Span::from(target.file()).with_range(target.focus_range()), + )); + } + + main.sub(sub); main } } + + pub(crate) enum GotoAction { + Definition, + Declaration, + TypeDefinition, + } + + impl GotoAction { + fn name(&self) -> &'static str { + match self { + GotoAction::Definition => "goto-definition", + GotoAction::Declaration => "goto-declaration", + GotoAction::TypeDefinition => "goto-type definition", + } + } + + fn label(&self) -> &'static str { + match self { + GotoAction::Definition => "Go to definition", + GotoAction::Declaration => "Go to declaration", + GotoAction::TypeDefinition => "Go to type definition", + } + } + + fn item_label(&self) -> &'static str { + match self { + GotoAction::Definition => "definition", + GotoAction::Declaration => "declaration", + GotoAction::TypeDefinition => "type definition", + } + } + } } diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index 53cc98413d..3d7f25f81f 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -28,15 +28,9 @@ pub fn goto_type_definition( #[cfg(test)] mod tests { - use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; - use crate::{NavigationTarget, goto_type_definition}; + use crate::goto_type_definition; + use crate::tests::{CursorTest, cursor_test}; use insta::assert_snapshot; - use ruff_db::diagnostic::{ - Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, - SubDiagnosticSeverity, - }; - use ruff_db::files::FileRange; - use ruff_text_size::Ranged; #[test] fn goto_type_of_expression_with_class_type() { @@ -49,21 +43,21 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:7 - | - 2 | class Test: ... - | ^^^^ - 3 | - 4 | ab = Test() - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | class Test: ... 3 | 4 | ab = Test() - | ^^ + | ^^ Clicking here + | + info: Found 1 type definition + --> main.py:2:7 + | + 2 | class Test: ... + | ---- + 3 | + 4 | ab = Test() | "); } @@ -79,23 +73,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> stdlib/typing.pyi:351:1 - | - 349 | Final: _SpecialForm - 350 | - 351 | Literal: _SpecialForm - | ^^^^^^^ - 352 | TypedDict: _SpecialForm - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | from typing import Literal 3 | 4 | ab = Literal - | ^^ + | ^^ Clicking here | + info: Found 1 type definition + --> stdlib/typing.pyi:351:1 + | + 349 | Final: _SpecialForm + 350 | + 351 | Literal: _SpecialForm + | ------- + 352 | TypedDict: _SpecialForm + | "); } @@ -112,23 +106,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/typing.pyi:166:7 - | - 164 | # from _typeshed import AnnotationForm - 165 | - 166 | class Any: - | ^^^ - 167 | """Special type indicating an unconstrained type. - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | from typing import Any 3 | 4 | ab = Any - | ^^ + | ^^ Clicking here | + info: Found 1 type definition + --> stdlib/typing.pyi:166:7 + | + 164 | # from _typeshed import AnnotationForm + 165 | + 166 | class Any: + | --- + 167 | """Special type indicating an unconstrained type. + | "#); } @@ -144,24 +138,24 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> stdlib/typing.pyi:781:1 - | - 779 | def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ... - 780 | - 781 | Generic: type[_Generic] - | ^^^^^^^ - 782 | - 783 | class _ProtocolMeta(ABCMeta): - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | from typing import Generic 3 | 4 | ab = Generic - | ^^ + | ^^ Clicking here | + info: Found 1 type definition + --> stdlib/typing.pyi:781:1 + | + 779 | def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ... + 780 | + 781 | Generic: type[_Generic] + | ------- + 782 | + 783 | class _ProtocolMeta(ABCMeta): + | "); } @@ -176,23 +170,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> stdlib/ty_extensions.pyi:21:1 - | - 19 | # Types - 20 | Unknown = object() - 21 | AlwaysTruthy = object() - | ^^^^^^^^^^^^ - 22 | AlwaysFalsy = object() - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | from ty_extensions import AlwaysTruthy 3 | 4 | ab = AlwaysTruthy - | ^^ + | ^^ Clicking here | + info: Found 1 type definition + --> stdlib/ty_extensions.pyi:21:1 + | + 19 | # Types + 20 | Unknown = object() + 21 | AlwaysTruthy = object() + | ------------ + 22 | AlwaysFalsy = object() + | "); } @@ -209,21 +203,21 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:5 - | - 2 | def foo(a, b): ... - | ^^^ - 3 | - 4 | ab = foo - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:6:1 | 4 | ab = foo 5 | 6 | ab - | ^^ + | ^^ Clicking here + | + info: Found 1 type definition + --> main.py:2:5 + | + 2 | def foo(a, b): ... + | --- + 3 | + 4 | ab = foo | "); } @@ -247,41 +241,25 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> main.py:12:1 + | + 10 | a = bar + 11 | + 12 | a + | ^ Clicking here + | + info: Found 2 type definitions --> main.py:3:5 | 3 | def foo(a, b): ... - | ^^^ + | --- 4 | 5 | def bar(a, b): ... - | - info: Source - --> main.py:12:1 - | - 10 | a = bar - 11 | - 12 | a - | ^ - | - - info[goto-type-definition]: Type definition - --> main.py:5:5 - | - 3 | def foo(a, b): ... - 4 | - 5 | def bar(a, b): ... - | ^^^ + | --- 6 | 7 | if random.choice(): | - info: Source - --> main.py:12:1 - | - 10 | a = bar - 11 | - 12 | a - | ^ - | "); } @@ -296,17 +274,17 @@ mod tests { test.write_file("lib.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib.py:1:1 - | - 1 | a = 10 - | ^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:8 | 2 | import lib - | ^^^ + | ^^^ Clicking here + | + info: Found 1 type definition + --> lib.py:1:1 + | + 1 | a = 10 + | ------ | "); } @@ -323,17 +301,17 @@ mod tests { test.write_file("lib/submod.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/__init__.py:1:1 - | - 1 | b = 7 - | ^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:8 | 2 | import lib.submod - | ^^^ + | ^^^ Clicking here + | + info: Found 1 type definition + --> lib/__init__.py:1:1 + | + 1 | b = 7 + | ----- | "); } @@ -350,17 +328,17 @@ mod tests { test.write_file("lib/submod.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/submod.py:1:1 - | - 1 | a = 10 - | ^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:12 | 2 | import lib.submod - | ^^^^^^ + | ^^^^^^ Clicking here + | + info: Found 1 type definition + --> lib/submod.py:1:1 + | + 1 | a = 10 + | ------ | "); } @@ -376,17 +354,17 @@ mod tests { test.write_file("lib.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib.py:1:1 - | - 1 | a = 10 - | ^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:6 | 2 | from lib import a - | ^^^ + | ^^^ Clicking here + | + info: Found 1 type definition + --> lib.py:1:1 + | + 1 | a = 10 + | ------ | "); } @@ -403,17 +381,17 @@ mod tests { test.write_file("lib/submod.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/__init__.py:1:1 - | - 1 | b = 7 - | ^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:6 | 2 | from lib.submod import a - | ^^^ + | ^^^ Clicking here + | + info: Found 1 type definition + --> lib/__init__.py:1:1 + | + 1 | b = 7 + | ----- | "); } @@ -430,17 +408,17 @@ mod tests { test.write_file("lib/submod.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/submod.py:1:1 - | - 1 | a = 10 - | ^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:10 | 2 | from lib.submod import a - | ^^^^^^ + | ^^^^^^ Clicking here + | + info: Found 1 type definition + --> lib/submod.py:1:1 + | + 1 | a = 10 + | ------ | "); } @@ -466,19 +444,19 @@ mod tests { .unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/sub/bot/botmod.py:1:1 - | - 1 | botmod = 31 - | ^^^^^^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> lib/sub/__init__.py:2:11 | 2 | from .bot.botmod import * - | ^^^^^^ + | ^^^^^^ Clicking here 3 | sub = 2 | + info: Found 1 type definition + --> lib/sub/bot/botmod.py:1:1 + | + 1 | botmod = 31 + | ----------- + | "); } @@ -503,19 +481,19 @@ mod tests { .unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/sub/bot/__init__.py:1:1 - | - 1 | bot = 3 - | ^^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> lib/sub/__init__.py:2:7 | 2 | from .bot.botmod import * - | ^^^ + | ^^^ Clicking here 3 | sub = 2 | + info: Found 1 type definition + --> lib/sub/bot/__init__.py:1:1 + | + 1 | bot = 3 + | ------- + | "); } @@ -540,19 +518,19 @@ mod tests { .unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib/sub/bot/__init__.py:1:1 - | - 1 | bot = 3 - | ^^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> lib/sub/__init__.py:2:7 | 2 | from .bot.botmod import * - | ^^^ + | ^^^ Clicking here 3 | sub = 2 | + info: Found 1 type definition + --> lib/sub/bot/__init__.py:1:1 + | + 1 | bot = 3 + | ------- + | "); } @@ -592,19 +570,19 @@ mod tests { test.write_file("lib.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib.py:1:1 - | - 1 | a = 10 - | ^^^^^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | import lib 3 | 4 | lib - | ^^^ + | ^^^ Clicking here + | + info: Found 1 type definition + --> lib.py:1:1 + | + 1 | a = 10 + | ------ | "); } @@ -620,23 +598,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:915:7 - | - 914 | @disjoint_base - 915 | class str(Sequence[str]): - | ^^^ - 916 | """str(object='') -> str - 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | a: str = "test" 3 | 4 | a - | ^ + | ^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | --- + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | "#); } #[test] @@ -648,21 +626,21 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> main.py:2:10 + | + 2 | a: str = "test" + | ^^^^^^ Clicking here + | + info: Found 1 type definition --> stdlib/builtins.pyi:915:7 | 914 | @disjoint_base 915 | class str(Sequence[str]): - | ^^^ + | --- 916 | """str(object='') -> str 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | - info: Source - --> main.py:2:10 - | - 2 | a: str = "test" - | ^^^^^^ - | "#); } @@ -675,17 +653,17 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:12 - | - 2 | type Alias[T: int = bool] = list[T] - | ^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:34 | 2 | type Alias[T: int = bool] = list[T] - | ^ + | ^ Clicking here + | + info: Found 1 type definition + --> main.py:2:12 + | + 2 | type Alias[T: int = bool] = list[T] + | - | "); } @@ -699,17 +677,17 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:14 - | - 2 | type Alias[**P = [int, str]] = Callable[P, int] - | ^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:41 | 2 | type Alias[**P = [int, str]] = Callable[P, int] - | ^ + | ^ Clicking here + | + info: Found 1 type definition + --> main.py:2:14 + | + 2 | type Alias[**P = [int, str]] = Callable[P, int] + | - | "); } @@ -756,23 +734,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> main.py:2:5 + | + 2 | a: "MyClass" = 1 + | ^^^^^^^ Clicking here + 3 | + 4 | class MyClass: + | + info: Found 1 type definition --> main.py:4:7 | 2 | a: "MyClass" = 1 3 | 4 | class MyClass: - | ^^^^^^^ + | ------- 5 | """some docs""" | - info: Source - --> main.py:2:5 - | - 2 | a: "MyClass" = 1 - | ^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -802,41 +780,31 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> main.py:4:7 - | - 2 | a: "None | MyClass" = 1 - 3 | - 4 | class MyClass: - | ^^^^^^^ - 5 | """some docs""" - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:4 | 2 | a: "None | MyClass" = 1 - | ^^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^^ Clicking here 3 | 4 | class MyClass: | - - info[goto-type-definition]: Type definition - --> stdlib/types.pyi:950:11 + info: Found 2 type definitions + --> main.py:4:7 + | + 2 | a: "None | MyClass" = 1 + 3 | + 4 | class MyClass: + | ------- + 5 | """some docs""" + | + ::: stdlib/types.pyi:950:11 | 948 | if sys.version_info >= (3, 10): 949 | @final 950 | class NoneType: - | ^^^^^^^^ + | -------- 951 | """The type of the None singleton.""" | - info: Source - --> main.py:2:4 - | - 2 | a: "None | MyClass" = 1 - | ^^^^^^^^^^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -866,41 +834,31 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> main.py:4:7 - | - 2 | a: "None | MyClass" = 1 - 3 | - 4 | class MyClass: - | ^^^^^^^ - 5 | """some docs""" - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:4 | 2 | a: "None | MyClass" = 1 - | ^^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^^ Clicking here 3 | 4 | class MyClass: | - - info[goto-type-definition]: Type definition - --> stdlib/types.pyi:950:11 + info: Found 2 type definitions + --> main.py:4:7 + | + 2 | a: "None | MyClass" = 1 + 3 | + 4 | class MyClass: + | ------- + 5 | """some docs""" + | + ::: stdlib/types.pyi:950:11 | 948 | if sys.version_info >= (3, 10): 949 | @final 950 | class NoneType: - | ^^^^^^^^ + | -------- 951 | """The type of the None singleton.""" | - info: Source - --> main.py:2:4 - | - 2 | a: "None | MyClass" = 1 - | ^^^^^^^^^^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -916,23 +874,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> main.py:2:4 + | + 2 | a: "MyClass |" = 1 + | ^^^^^^^^^^^ Clicking here + 3 | + 4 | class MyClass: + | + info: Found 1 type definition --> stdlib/ty_extensions.pyi:20:1 | 19 | # Types 20 | Unknown = object() - | ^^^^^^^ + | ------- 21 | AlwaysTruthy = object() 22 | AlwaysFalsy = object() | - info: Source - --> main.py:2:4 - | - 2 | a: "MyClass |" = 1 - | ^^^^^^^^^^^ - 3 | - 4 | class MyClass: - | "#); } @@ -973,21 +931,21 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> main.py:2:6 + | + 2 | ab: "ab" + | ^^ Clicking here + | + info: Found 1 type definition --> stdlib/ty_extensions.pyi:20:1 | 19 | # Types 20 | Unknown = object() - | ^^^^^^^ + | ------- 21 | AlwaysTruthy = object() 22 | AlwaysFalsy = object() | - info: Source - --> main.py:2:6 - | - 2 | ab: "ab" - | ^^ - | "#); } @@ -1000,21 +958,21 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> main.py:2:5 + | + 2 | x: "foobar" + | ^^^^^^ Clicking here + | + info: Found 1 type definition --> stdlib/ty_extensions.pyi:20:1 | 19 | # Types 20 | Unknown = object() - | ^^^^^^^ + | ------- 21 | AlwaysTruthy = object() 22 | AlwaysFalsy = object() | - info: Source - --> main.py:2:5 - | - 2 | x: "foobar" - | ^^^^^^ - | "#); } @@ -1160,23 +1118,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> main.py:2:7 - | - 2 | class Click: - | ^^^^^ - 3 | __match_args__ = ("position", "button") - 4 | def __init__(self, pos, btn): - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:10:14 | 8 | def my_func(event: Click): 9 | match event: 10 | case Click(x, button=ab): - | ^^^^^ + | ^^^^^ Clicking here 11 | x = ab | + info: Found 1 type definition + --> main.py:2:7 + | + 2 | class Click: + | ----- + 3 | __match_args__ = ("position", "button") + 4 | def __init__(self, pos, btn): + | "#); } @@ -1209,17 +1167,17 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | ^^ Clicking here | - info: Source + info: Found 1 type definition --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | -- | "); } @@ -1233,17 +1191,17 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:13 - | - 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:2:37 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] - | ^^ + | ^^ Clicking here + | + info: Found 1 type definition + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | -- | "); } @@ -1305,23 +1263,23 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:915:7 - | - 914 | @disjoint_base - 915 | class str(Sequence[str]): - | ^^^ - 916 | """str(object='') -> str - 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:6 | 2 | def test(a: str): ... 3 | 4 | test(a= "123") - | ^ + | ^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | --- + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | "#); } @@ -1339,23 +1297,23 @@ mod tests { // the keyword is typed as a string. It's only the passed argument that // is an int. Navigating to `str` would match pyright's behavior. assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:348:7 - | - 347 | @disjoint_base - 348 | class int: - | ^^^ - 349 | """int([x]) -> integer - 350 | int(x, base=10) -> integer - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:6 | 2 | def test(a: str): ... 3 | 4 | test(a= 123) - | ^ + | ^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:348:7 + | + 347 | @disjoint_base + 348 | class int: + | --- + 349 | """int([x]) -> integer + 350 | int(x, base=10) -> integer + | "#); } @@ -1372,23 +1330,23 @@ f(**kwargs) ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:2920:7 - | - 2919 | @disjoint_base - 2920 | class dict(MutableMapping[_KT, _VT]): - | ^^^^ - 2921 | """dict() -> new empty dictionary - 2922 | dict(mapping) -> new dictionary initialized from a mapping object's - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:6:5 | 4 | kwargs = { "name": "test"} 5 | 6 | f(**kwargs) - | ^^^^^^ + | ^^^^^^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:2920:7 + | + 2919 | @disjoint_base + 2920 | class dict(MutableMapping[_KT, _VT]): + | ---- + 2921 | """dict() -> new empty dictionary + 2922 | dict(mapping) -> new dictionary initialized from a mapping object's + | "#); } @@ -1410,25 +1368,25 @@ def outer(): // Should find the variable declaration in the outer scope, not the nonlocal statement assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:915:7 - | - 914 | @disjoint_base - 915 | class str(Sequence[str]): - | ^^^ - 916 | """str(object='') -> str - 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:8:16 | 6 | nonlocal x 7 | x = "modified" 8 | return x # Should find the nonlocal x declaration in outer scope - | ^ + | ^ Clicking here 9 | 10 | return inner | + info: Found 1 type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | --- + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | "#); } @@ -1467,23 +1425,23 @@ def function(): // Should find the global variable declaration, not the global statement assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:915:7 - | - 914 | @disjoint_base - 915 | class str(Sequence[str]): - | ^^^ - 916 | """str(object='') -> str - 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:7:12 | 5 | global global_var 6 | global_var = "modified" 7 | return global_var # Should find the global variable declaration - | ^^^^^^^^^^ + | ^^^^^^^^^^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | --- + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | "#); } @@ -1514,22 +1472,22 @@ def function(): ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:915:7 - | - 914 | @disjoint_base - 915 | class str(Sequence[str]): - | ^^^ - 916 | """str(object='') -> str - 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:3:5 | 2 | def foo(a: str): 3 | a - | ^ + | ^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | --- + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | "#); } @@ -1547,20 +1505,20 @@ def function(): ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:7 - | - 2 | class X: - | ^ - 3 | def foo(a, b): ... - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:7:1 | 5 | x = X() 6 | 7 | x.foo() - | ^ + | ^ Clicking here + | + info: Found 1 type definition + --> main.py:2:7 + | + 2 | class X: + | - + 3 | def foo(a, b): ... | "); } @@ -1576,21 +1534,21 @@ def function(): ); assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:5 - | - 2 | def foo(a, b): ... - | ^^^ - 3 | - 4 | foo() - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:1 | 2 | def foo(a, b): ... 3 | 4 | foo() - | ^^^ + | ^^^ Clicking here + | + info: Found 1 type definition + --> main.py:2:5 + | + 2 | def foo(a, b): ... + | --- + 3 | + 4 | foo() | "); } @@ -1606,23 +1564,23 @@ def function(): ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:915:7 - | - 914 | @disjoint_base - 915 | class str(Sequence[str]): - | ^^^ - 916 | """str(object='') -> str - 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:4:15 | 2 | def foo(a: str | None, b): 3 | if a is not None: 4 | print(a) - | ^ + | ^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | --- + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | "#); } @@ -1636,39 +1594,30 @@ def function(): ); assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/types.pyi:950:11 - | - 948 | if sys.version_info >= (3, 10): - 949 | @final - 950 | class NoneType: - | ^^^^^^^^ - 951 | """The type of the None singleton.""" - | - info: Source + info[goto-type definition]: Go to type definition --> main.py:3:5 | 2 | def foo(a: str | None, b): 3 | a - | ^ + | ^ Clicking here | - - info[goto-type-definition]: Type definition + info: Found 2 type definitions --> stdlib/builtins.pyi:915:7 | 914 | @disjoint_base 915 | class str(Sequence[str]): - | ^^^ + | --- 916 | """str(object='') -> str 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | - info: Source - --> main.py:3:5 - | - 2 | def foo(a: str | None, b): - 3 | a - | ^ - | + ::: stdlib/types.pyi:950:11 + | + 948 | if sys.version_info >= (3, 10): + 949 | @final + 950 | class NoneType: + | -------- + 951 | """The type of the None singleton.""" + | "#); } @@ -1694,18 +1643,18 @@ def function(): // The module is the correct type definition assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> mypackage/subpkg/__init__.py:1:1 - | - | - info: Source + info[goto-type definition]: Go to type definition --> mypackage/__init__.py:4:5 | 2 | from .subpkg.submod import val 3 | 4 | x = subpkg - | ^^^^^^ + | ^^^^^^ Clicking here | + info: Found 1 type definition + --> mypackage/subpkg/__init__.py:1:1 + | + | "); } @@ -1731,18 +1680,18 @@ def function(): // The module is the correct type definition assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> mypackage/subpkg/__init__.py:1:1 - | - | - info: Source + info[goto-type definition]: Go to type definition --> mypackage/__init__.py:2:7 | 2 | from .subpkg.submod import val - | ^^^^^^ + | ^^^^^^ Clicking here 3 | 4 | x = subpkg | + info: Found 1 type definition + --> mypackage/subpkg/__init__.py:1:1 + | + | "); } @@ -1768,23 +1717,23 @@ def function(): // Unknown is correct, `submod` is not in scope assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> stdlib/ty_extensions.pyi:20:1 - | - 19 | # Types - 20 | Unknown = object() - | ^^^^^^^ - 21 | AlwaysTruthy = object() - 22 | AlwaysFalsy = object() - | - info: Source + info[goto-type definition]: Go to type definition --> mypackage/__init__.py:4:5 | 2 | from .subpkg.submod import val 3 | 4 | x = submod - | ^^^^^^ + | ^^^^^^ Clicking here | + info: Found 1 type definition + --> stdlib/ty_extensions.pyi:20:1 + | + 19 | # Types + 20 | Unknown = object() + | ------- + 21 | AlwaysTruthy = object() + 22 | AlwaysFalsy = object() + | "); } @@ -1810,20 +1759,20 @@ def function(): // The module is correct assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> mypackage/__init__.py:2:14 + | + 2 | from .subpkg.submod import val + | ^^^^^^ Clicking here + 3 | + 4 | x = submod + | + info: Found 1 type definition --> mypackage/subpkg/submod.py:1:1 | 1 | / 2 | | val: int = 0 - | |_____________^ - | - info: Source - --> mypackage/__init__.py:2:14 - | - 2 | from .subpkg.submod import val - | ^^^^^^ - 3 | - 4 | x = submod + | |_____________- | "); } @@ -1849,20 +1798,20 @@ def function(): // The module is correct assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> mypackage/__init__.py:2:7 + | + 2 | from .subpkg import subpkg + | ^^^^^^ Clicking here + 3 | + 4 | x = subpkg + | + info: Found 1 type definition --> mypackage/subpkg/__init__.py:1:1 | 1 | / 2 | | subpkg: int = 10 - | |_________________^ - | - info: Source - --> mypackage/__init__.py:2:7 - | - 2 | from .subpkg import subpkg - | ^^^^^^ - 3 | - 4 | x = subpkg + | |_________________- | "); } @@ -1888,23 +1837,23 @@ def function(): // `int` is correct assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition + info[goto-type definition]: Go to type definition + --> mypackage/__init__.py:2:21 + | + 2 | from .subpkg import subpkg + | ^^^^^^ Clicking here + 3 | + 4 | x = subpkg + | + info: Found 1 type definition --> stdlib/builtins.pyi:348:7 | 347 | @disjoint_base 348 | class int: - | ^^^ + | --- 349 | """int([x]) -> integer 350 | int(x, base=10) -> integer | - info: Source - --> mypackage/__init__.py:2:21 - | - 2 | from .subpkg import subpkg - | ^^^^^^ - 3 | - 4 | x = subpkg - | "#); } @@ -1929,31 +1878,31 @@ def function(): // `int` is correct assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:348:7 - | - 347 | @disjoint_base - 348 | class int: - | ^^^ - 349 | """int([x]) -> integer - 350 | int(x, base=10) -> integer - | - info: Source + info[goto-type definition]: Go to type definition --> mypackage/__init__.py:4:5 | 2 | from .subpkg import subpkg 3 | 4 | x = subpkg - | ^^^^^^ + | ^^^^^^ Clicking here | + info: Found 1 type definition + --> stdlib/builtins.pyi:348:7 + | + 347 | @disjoint_base + 348 | class int: + | --- + 349 | """int([x]) -> integer + 350 | int(x, base=10) -> integer + | "#); } impl CursorTest { fn goto_type_definition(&self) -> String { - let Some(targets) = + let Some(targets) = salsa::attach(&self.db, || { goto_type_definition(&self.db, self.cursor.file, self.cursor.offset) - else { + }) else { return "No goto target found".to_string(); }; @@ -1961,47 +1910,10 @@ def function(): return "No type definitions found".to_string(); } - let source = targets.range; - self.render_diagnostics( - targets - .into_iter() - .map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)), - ) - } - } - - struct GotoTypeDefinitionDiagnostic { - source: FileRange, - target: FileRange, - } - - impl GotoTypeDefinitionDiagnostic { - fn new(source: FileRange, target: &NavigationTarget) -> Self { - Self { - source, - target: FileRange::new(target.file(), target.focus_range()), - } - } - } - - impl IntoDiagnostic for GotoTypeDefinitionDiagnostic { - fn into_diagnostic(self) -> Diagnostic { - let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source"); - source.annotate(Annotation::primary( - Span::from(self.source.file()).with_range(self.source.range()), - )); - - let mut main = Diagnostic::new( - DiagnosticId::Lint(LintName::of("goto-type-definition")), - Severity::Info, - "Type definition".to_string(), - ); - main.annotate(Annotation::primary( - Span::from(self.target.file()).with_range(self.target.range()), - )); - main.sub(source); - - main + self.render_diagnostics([crate::goto_definition::test::GotoDiagnostic::new( + crate::goto_definition::test::GotoAction::TypeDefinition, + targets, + )]) } } } diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 8f9add508a..8430a46edc 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -3610,6 +3610,20 @@ def function(): "); } + #[test] + fn hover_tuple_assignment_target() { + let test = CursorTest::builder() + .source( + "test.py", + r#" + (x, y) = "test", 10 + "#, + ) + .build(); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + impl CursorTest { fn hover(&self) -> String { use std::fmt::Write; diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index f3dd178786..d0742f58ec 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -362,8 +362,9 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> { Expr::Name(name) => { if let Some(rhs) = self.assignment_rhs { if name.ctx.is_store() { - let ty = expr.inferred_type(&self.model); - self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed); + if let Some(ty) = expr.inferred_type(&self.model) { + self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed); + } } } source_order::walk_expr(self, expr); @@ -371,8 +372,9 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> { Expr::Attribute(attribute) => { if let Some(rhs) = self.assignment_rhs { if attribute.ctx.is_store() { - let ty = expr.inferred_type(&self.model); - self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed); + if let Some(ty) = expr.inferred_type(&self.model) { + self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed); + } } } source_order::walk_expr(self, expr); diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 28989dcf8f..7d65984f8e 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -230,6 +230,11 @@ impl NavigationTargets { fn is_empty(&self) -> bool { self.0.is_empty() } + + #[cfg(test)] + fn len(&self) -> usize { + self.0.len() + } } impl IntoIterator for NavigationTargets { diff --git a/crates/ty_ide/src/rename.rs b/crates/ty_ide/src/rename.rs index fe51f06615..ecea0ffc36 100644 --- a/crates/ty_ide/src/rename.rs +++ b/crates/ty_ide/src/rename.rs @@ -98,7 +98,9 @@ mod tests { impl CursorTest { fn prepare_rename(&self) -> String { - let Some(range) = can_rename(&self.db, self.cursor.file, self.cursor.offset) else { + let Some(range) = salsa::attach(&self.db, || { + can_rename(&self.db, self.cursor.file, self.cursor.offset) + }) else { return "Cannot rename".to_string(); }; @@ -106,13 +108,13 @@ mod tests { } fn rename(&self, new_name: &str) -> String { - let Some(_) = can_rename(&self.db, self.cursor.file, self.cursor.offset) else { - return "Cannot rename".to_string(); - }; + let rename_results = salsa::attach(&self.db, || { + can_rename(&self.db, self.cursor.file, self.cursor.offset)?; - let Some(rename_results) = rename(&self.db, self.cursor.file, self.cursor.offset, new_name) - else { + }); + + let Some(rename_results) = rename_results else { return "Cannot rename".to_string(); }; diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index 5667d1506f..79b37265aa 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -273,7 +273,7 @@ impl<'db> SemanticTokenVisitor<'db> { } // Fall back to type-based classification. - let ty = name.inferred_type(self.model); + let ty = name.inferred_type(self.model).unwrap_or(Type::unknown()); let name_str = name.id.as_str(); self.classify_from_type_and_name_str(ty, name_str) } @@ -302,7 +302,9 @@ impl<'db> SemanticTokenVisitor<'db> { let parsed = parsed_module(db, definition.file(db)); let ty = parameter.node(&parsed.load(db)).inferred_type(&model); - if let Type::TypeVar(type_var) = ty { + if let Some(ty) = ty + && let Type::TypeVar(type_var) = ty + { match type_var.typevar(db).kind(db) { TypeVarKind::TypingSelf => { return Some((SemanticTokenType::SelfParameter, modifiers)); @@ -344,9 +346,9 @@ impl<'db> SemanticTokenVisitor<'db> { _ => None, }; - if let Some(value) = value { - let value_ty = value.inferred_type(&model); - + if let Some(value) = value + && let Some(value_ty) = value.inferred_type(&model) + { if value_ty.is_class_literal() || value_ty.is_subclass_of() || value_ty.is_generic_alias() @@ -710,12 +712,12 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> { for alias in &import.names { if let Some(asname) = &alias.asname { // For aliased imports (from X import Y as Z), classify Z based on what Y is - let ty = alias.inferred_type(self.model); + let ty = alias.inferred_type(self.model).unwrap_or(Type::unknown()); let (token_type, modifiers) = self.classify_from_alias_type(ty, asname); self.add_token(asname, token_type, modifiers); } else { // For direct imports (from X import Y), use semantic classification - let ty = alias.inferred_type(self.model); + let ty = alias.inferred_type(self.model).unwrap_or(Type::unknown()); let (token_type, modifiers) = self.classify_from_alias_type(ty, &alias.name); self.add_token(&alias.name, token_type, modifiers); @@ -835,7 +837,7 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> { self.visit_expr(&attr.value); // Then add token for the attribute name (e.g., 'path' in 'os.path') - let ty = expr.inferred_type(self.model); + let ty = expr.inferred_type(self.model).unwrap_or(Type::unknown()); let (token_type, modifiers) = Self::classify_from_type_for_attribute(ty, &attr.attr); self.add_token(&attr.attr, token_type, modifiers); diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/string.md b/crates/ty_python_semantic/resources/mdtest/annotations/string.md index 5777070441..db152ae907 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/string.md @@ -156,6 +156,9 @@ a: "1 or 2" b: "(x := 1)" # error: [invalid-type-form] c: "1 + 2" +# Regression test for https://github.com/astral-sh/ty/issues/1847 +# error: [invalid-type-form] +c2: "a*(i for i in [])" d: "lambda x: x" e: "x if True else y" f: "{'a': 1, 'b': 2}" diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md index d800c86c6a..cec8c6de43 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md @@ -44,7 +44,7 @@ class C: return 42 x = C() -# error: [unsupported-operator] "Operator `-=` is unsupported between objects of type `C` and `Literal[1]`" +# error: [unsupported-operator] "Operator `-=` is not supported between objects of type `C` and `Literal[1]`" x -= 1 reveal_type(x) # revealed: int @@ -79,7 +79,7 @@ def _(flag: bool): f = Foo() - # error: [unsupported-operator] "Operator `+=` is unsupported between objects of type `Foo` and `Literal["Hello, world!"]`" + # error: [unsupported-operator] "Operator `+=` is not supported between objects of type `Foo` and `Literal["Hello, world!"]`" f += "Hello, world!" reveal_type(f) # revealed: int | Unknown diff --git a/crates/ty_python_semantic/resources/mdtest/binary/classes.md b/crates/ty_python_semantic/resources/mdtest/binary/classes.md index 4a3580a8de..e0da23bf50 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/classes.md @@ -27,7 +27,7 @@ python-version = "3.9" class A: ... class B: ... -# error: "Operator `|` is unsupported between objects of type `` and ``" +# error: "Operator `|` is not supported between objects of type `` and ``" reveal_type(A | B) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/custom.md b/crates/ty_python_semantic/resources/mdtest/binary/custom.md index d2587b7a75..9bd0852253 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/custom.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/custom.md @@ -79,59 +79,59 @@ reveal_type(Sub() & Sub()) # revealed: Literal["&"] reveal_type(Sub() // Sub()) # revealed: Literal["//"] # No does not implement any of the dunder methods. -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `No` and `No`" reveal_type(No() + No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `-` is not supported between objects of type `No` and `No`" reveal_type(No() - No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `*` is not supported between objects of type `No` and `No`" reveal_type(No() * No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `@` is not supported between objects of type `No` and `No`" reveal_type(No() @ No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `/` is not supported between objects of type `No` and `No`" reveal_type(No() / No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `%` is not supported between objects of type `No` and `No`" reveal_type(No() % No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `**` is not supported between objects of type `No` and `No`" reveal_type(No() ** No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `<<` is not supported between objects of type `No` and `No`" reveal_type(No() << No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `>>` is not supported between objects of type `No` and `No`" reveal_type(No() >> No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `|` is not supported between objects of type `No` and `No`" reveal_type(No() | No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `^` is not supported between objects of type `No` and `No`" reveal_type(No() ^ No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `&` is not supported between objects of type `No` and `No`" reveal_type(No() & No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `//` is not supported between objects of type `No` and `No`" reveal_type(No() // No()) # revealed: Unknown # Yes does not implement any of the reflected dunder methods. -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `No` and `Yes`" reveal_type(No() + Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `-` is not supported between objects of type `No` and `Yes`" reveal_type(No() - Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `*` is not supported between objects of type `No` and `Yes`" reveal_type(No() * Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `@` is not supported between objects of type `No` and `Yes`" reveal_type(No() @ Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `/` is not supported between objects of type `No` and `Yes`" reveal_type(No() / Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `%` is not supported between objects of type `No` and `Yes`" reveal_type(No() % Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `**` is not supported between objects of type `No` and `Yes`" reveal_type(No() ** Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `<<` is not supported between objects of type `No` and `Yes`" reveal_type(No() << Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `>>` is not supported between objects of type `No` and `Yes`" reveal_type(No() >> Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `|` is not supported between objects of type `No` and `Yes`" reveal_type(No() | Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `^` is not supported between objects of type `No` and `Yes`" reveal_type(No() ^ Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `&` is not supported between objects of type `No` and `Yes`" reveal_type(No() & Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`" +# error: [unsupported-operator] "Operator `//` is not supported between objects of type `No` and `Yes`" reveal_type(No() // Yes()) # revealed: Unknown ``` @@ -307,11 +307,11 @@ class Yes: class Sub(Yes): ... class No: ... -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `` and ``" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `` and ``" reveal_type(Yes + Yes) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `` and ``" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `` and ``" reveal_type(Sub + Sub) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `` and ``" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `` and ``" reveal_type(No + No) # revealed: Unknown ``` @@ -336,11 +336,11 @@ def sub() -> type[Sub]: def no() -> type[No]: return No -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `type[Yes]` and `type[Yes]`" reveal_type(yes() + yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `type[Sub]` and `type[Sub]`" reveal_type(sub() + sub()) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `type[No]` and `type[No]`" reveal_type(no() + no()) # revealed: Unknown ``` @@ -350,30 +350,30 @@ reveal_type(no() + no()) # revealed: Unknown def f(): pass -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f + f) # revealed: Unknown -# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `-` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f - f) # revealed: Unknown -# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `*` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f * f) # revealed: Unknown -# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `@` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f @ f) # revealed: Unknown -# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `/` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f / f) # revealed: Unknown -# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `%` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f % f) # revealed: Unknown -# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `**` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f**f) # revealed: Unknown -# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `<<` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f << f) # revealed: Unknown -# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `>>` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f >> f) # revealed: Unknown -# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `|` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f | f) # revealed: Unknown -# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `^` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f ^ f) # revealed: Unknown -# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `&` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f & f) # revealed: Unknown -# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `//` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" reveal_type(f // f) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/instances.md b/crates/ty_python_semantic/resources/mdtest/binary/instances.md index c981a570b0..c1cc6f924e 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/instances.md @@ -386,7 +386,7 @@ class A(metaclass=Meta): ... class B(metaclass=Meta): ... reveal_type(A + B) # revealed: int -# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `` and ``" +# error: [unsupported-operator] "Operator `-` is not supported between objects of type `` and ``" reveal_type(A - B) # revealed: Unknown reveal_type(A < B) # revealed: bool @@ -412,7 +412,7 @@ class A: def __init__(self): self.__add__ = add_impl -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `A` and `A`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `A` and `A`" # revealed: Unknown reveal_type(A() + A()) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/integers.md b/crates/ty_python_semantic/resources/mdtest/binary/integers.md index a021a15ae1..401c09d756 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/integers.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/integers.md @@ -13,7 +13,7 @@ reveal_type(3 | 4) # revealed: Literal[7] reveal_type(5 & 6) # revealed: Literal[4] reveal_type(7 ^ 2) # revealed: Literal[5] -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[2]` and `Literal["f"]`" +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `Literal[2]` and `Literal["f"]`" reveal_type(2 + "f") # revealed: Unknown def lhs(x: int): diff --git a/crates/ty_python_semantic/resources/mdtest/binary/unions.md b/crates/ty_python_semantic/resources/mdtest/binary/unions.md index 1ec0794cc4..1b980170d4 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/unions.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/unions.md @@ -5,9 +5,9 @@ combinations of types: ```py def f1(i: int, u: int | None): - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int` and `int | None`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int` and `int | None`" reveal_type(i + u) # revealed: Unknown - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | None` and `int`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int | None` and `int`" reveal_type(u + i) # revealed: Unknown ``` @@ -18,7 +18,7 @@ cannot be added, because that would require addition of `int` and `str` or vice def f2(i: int, s: str, int_or_str: int | str): i + i s + s - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int | str` and `int | str`" reveal_type(int_or_str + int_or_str) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md b/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md index 48b912cce1..082e0d43db 100644 --- a/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md +++ b/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md @@ -42,6 +42,6 @@ def _(flag: bool): class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" 3 if NotBoolable() else 4 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md b/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md index c7a8c7732b..f55dc41160 100644 --- a/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md +++ b/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md @@ -154,10 +154,10 @@ def _(flag: bool): class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" if NotBoolable(): ... -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" elif NotBoolable(): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/conditional/match.md b/crates/ty_python_semantic/resources/mdtest/conditional/match.md index c0df166e03..492ca8ef53 100644 --- a/crates/ty_python_semantic/resources/mdtest/conditional/match.md +++ b/crates/ty_python_semantic/resources/mdtest/conditional/match.md @@ -378,7 +378,7 @@ class NotBoolable: def _(target: int, flag: NotBoolable): y = 1 match target: - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" case 1 if flag: y = 2 case 2: diff --git a/crates/ty_python_semantic/resources/mdtest/expression/assert.md b/crates/ty_python_semantic/resources/mdtest/expression/assert.md index ddb429a576..6fd9e7700f 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/assert.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/assert.md @@ -4,6 +4,6 @@ class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" assert NotBoolable() ``` diff --git a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md index 413acf8e39..67ba0f4f05 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md @@ -232,7 +232,7 @@ if NotBoolable(): class NotBoolable: __bool__: None = None -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" if NotBoolable(): ... ``` @@ -244,7 +244,7 @@ def test(cond: bool): class NotBoolable: __bool__: int | None = None if cond else 3 - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" if NotBoolable(): ... ``` @@ -258,7 +258,7 @@ def test(cond: bool): a = 10 if cond else NotBoolable() - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`" + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `Literal[10] | NotBoolable`" if a: ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index bc6ccdb7c1..30d6a89ec0 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -860,9 +860,6 @@ reveal_type(Sub) # revealed: U = TypeVar("U") class Base2(Generic[T, U]): ... - -# TODO: no error -# error: [unsupported-base] "Unsupported class base with type ` | `" class Sub2(Base2["Sub2", U]): ... ``` @@ -888,8 +885,6 @@ from typing_extensions import Generic, TypeVar T = TypeVar("T") -# TODO: no error "Unsupported class base with type ` | `" -# error: [unsupported-base] class Derived(list[Derived[T]], Generic[T]): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index 69e079ca65..f883d1fb1a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -277,7 +277,7 @@ T = TypeVar("T", int, str) def same_constrained_types(t1: T, t2: T) -> T: # TODO: no error - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T@same_constrained_types` and `T@same_constrained_types`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `T@same_constrained_types` and `T@same_constrained_types`" return t1 + t2 ``` @@ -287,7 +287,7 @@ and an `int` and a `str` cannot be added together: ```py def unions_are_different(t1: int | str, t2: int | str) -> int | str: - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int | str` and `int | str`" return t1 + t2 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index eedea0beaa..cb8f7dbc90 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -246,7 +246,7 @@ methods that are compatible with the return type, so the `return` expression is ```py def same_constrained_types[T: (int, str)](t1: T, t2: T) -> T: # TODO: no error - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T@same_constrained_types` and `T@same_constrained_types`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `T@same_constrained_types` and `T@same_constrained_types`" return t1 + t2 ``` @@ -256,7 +256,7 @@ and an `int` and a `str` cannot be added together: ```py def unions_are_different(t1: int | str, t2: int | str) -> int | str: - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`" + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int | str` and `int | str`" return t1 + t2 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 0886393143..99a5de8aa9 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -214,7 +214,7 @@ def _(int_or_int: IntOrInt, list_of_int_or_list_of_int: ListOfIntOrListOfInt): `NoneType` has no special or-operator behavior, so this is an error: ```py -None | None # error: [unsupported-operator] "Operator `|` is unsupported between objects of type `None` and `None`" +None | None # error: [unsupported-operator] "Operator `|` is not supported between objects of type `None` and `None`" ``` When constructing something nonsensical like `int | 1`, we emit a diagnostic for the expression diff --git a/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md b/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md index 5a5784b85d..41e48b1404 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md @@ -123,7 +123,7 @@ def _(flag: bool, flag2: bool): class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`" while NotBoolable(): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md index d6df7c3276..cb9f0c4545 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md @@ -270,7 +270,7 @@ def _( if af: reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`" + # error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `MetaDeferred`" if d: # TODO: Should be `Unknown` reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno…_-_PEP-604_in_non-type-…_-_Earlier_versions_(f2859c9800f37c7).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno…_-_PEP-604_in_non-type-…_-_Earlier_versions_(f2859c9800f37c7).snap index 3cfe98c8f7..83b964676a 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno…_-_PEP-604_in_non-type-…_-_Earlier_versions_(f2859c9800f37c7).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno…_-_PEP-604_in_non-type-…_-_Earlier_versions_(f2859c9800f37c7).snap @@ -19,7 +19,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/assignment/annotations.m # Diagnostics ``` -error[unsupported-operator]: Operator `|` is unsupported between objects of type `` and `` +error[unsupported-operator]: Operator `|` is not supported between objects of type `` and `` --> src/mdtest_snippet.py:2:12 | 1 | # error: [unsupported-operator] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on…_-_Operations_involving…_(492b1163b8163c05).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on…_-_Operations_involving…_(492b1163b8163c05).snap index ac85e00fa0..2fba47e078 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on…_-_Operations_involving…_(492b1163b8163c05).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on…_-_Operations_involving…_(492b1163b8163c05).snap @@ -24,7 +24,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/binary/instances.md # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:7:8 | 6 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membersh…_-_Return_type_that_doe…_(feccf6b9da1e7cd3).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membersh…_-_Return_type_that_doe…_(feccf6b9da1e7cd3).snap index d6698b03f4..26415ace68 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membersh…_-_Return_type_that_doe…_(feccf6b9da1e7cd3).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membersh…_-_Return_type_that_doe…_(feccf6b9da1e7cd3).snap @@ -28,7 +28,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/instances/mem # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:9:1 | 8 | # error: [unsupported-bool-conversion] @@ -43,7 +43,7 @@ info: rule `unsupported-bool-conversion` is enabled by default ``` ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:11:1 | 9 | 10 in WithContains() diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implemen…_(ab3f546bf004e24d).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implemen…_(ab3f546bf004e24d).snap index defb8528ec..f5e88ebed0 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implemen…_(ab3f546bf004e24d).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implemen…_(ab3f546bf004e24d).snap @@ -22,7 +22,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/unary/not.md # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:5:1 | 4 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Com…_-_Chained_comparisons_…_(c391c13e2abc18a0).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Com…_-_Chained_comparisons_…_(c391c13e2abc18a0).snap index e40ecc8361..f62a90156b 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Com…_-_Chained_comparisons_…_(c391c13e2abc18a0).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Com…_-_Chained_comparisons_…_(c391c13e2abc18a0).snap @@ -33,7 +33,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/instances/ric # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:12:1 | 11 | # error: [unsupported-bool-conversion] @@ -48,7 +48,7 @@ info: rule `unsupported-bool-conversion` is enabled by default ``` ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:14:1 | 12 | 10 < Comparable() < 20 diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_…_(f45f1da2f8ca693d).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_…_(f45f1da2f8ca693d).snap index e8fe6a5285..52bb93b61e 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_…_(f45f1da2f8ca693d).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_…_(f45f1da2f8ca693d).snap @@ -34,7 +34,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable | Literal[False]` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable | Literal[False]` --> src/mdtest_snippet.py:15:1 | 14 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen…_(39b614d4707c0661).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen…_(39b614d4707c0661).snap index 2c9fbd885a..c530be195b 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen…_(39b614d4707c0661).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen…_(39b614d4707c0661).snap @@ -58,7 +58,7 @@ info: rule `invalid-method-override` is enabled by default ``` ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:10:1 | 9 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_att…_(2721d40bf12fe8b7).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_att…_(2721d40bf12fe8b7).snap index 343aaccc77..bcb4ffb0c6 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_att…_(2721d40bf12fe8b7).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_att…_(2721d40bf12fe8b7).snap @@ -24,7 +24,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_ # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:7:8 | 6 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(15636dc4074e5335).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(15636dc4074e5335).snap index 09343ef5d6..b3a86fac93 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(15636dc4074e5335).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(15636dc4074e5335).snap @@ -25,7 +25,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_ # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:8:8 | 7 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(ce8b8da49eaf4cda).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(ce8b8da49eaf4cda).snap index 9957e4c64f..0e35519ee4 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(ce8b8da49eaf4cda).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Has_a_`__bool__`_met…_(ce8b8da49eaf4cda).snap @@ -25,7 +25,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_ # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` --> src/mdtest_snippet.py:8:8 | 7 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Part_of_a_union_wher…_(7cca8063ea43c1a).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Part_of_a_union_wher…_(7cca8063ea43c1a).snap index 22d0cc6ada..d86b17b1dc 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Part_of_a_union_wher…_(7cca8063ea43c1a).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con…_-_Different_ways_that_…_-_Part_of_a_union_wher…_(7cca8063ea43c1a).snap @@ -32,7 +32,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_ # Diagnostics ``` -error[unsupported-bool-conversion]: Boolean conversion is unsupported for union `NotBoolable1 | NotBoolable2 | NotBoolable3` because `NotBoolable1` doesn't implement `__bool__` correctly +error[unsupported-bool-conversion]: Boolean conversion is not supported for union `NotBoolable1 | NotBoolable2 | NotBoolable3` because `NotBoolable1` doesn't implement `__bool__` correctly --> src/mdtest_snippet.py:15:8 | 14 | # error: [unsupported-bool-conversion] diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index 9cb9ca40f4..09315697da 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -237,7 +237,7 @@ class InvalidBoolDunder: def __bool__(self) -> int: return 1 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `InvalidBoolDunder`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `InvalidBoolDunder`" static_assert(InvalidBoolDunder()) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/unary/custom.md b/crates/ty_python_semantic/resources/mdtest/unary/custom.md index 1544e42890..2b2eb3a619 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/custom.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/custom.md @@ -24,11 +24,11 @@ reveal_type(+Sub()) # revealed: bool reveal_type(-Sub()) # revealed: str reveal_type(~Sub()) # revealed: int -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `No`" +# error: [unsupported-operator] "Unary operator `+` is not supported for type `No`" reveal_type(+No()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `No`" +# error: [unsupported-operator] "Unary operator `-` is not supported for type `No`" reveal_type(-No()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `No`" +# error: [unsupported-operator] "Unary operator `~` is not supported for type `No`" reveal_type(~No()) # revealed: Unknown ``` @@ -52,25 +52,25 @@ class Yes: class Sub(Yes): ... class No: ... -# error: [unsupported-operator] "Unary operator `+` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `+` is not supported for type ``" reveal_type(+Yes) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `-` is not supported for type ``" reveal_type(-Yes) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `~` is not supported for type ``" reveal_type(~Yes) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `+` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `+` is not supported for type ``" reveal_type(+Sub) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `-` is not supported for type ``" reveal_type(-Sub) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `~` is not supported for type ``" reveal_type(~Sub) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `+` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `+` is not supported for type ``" reveal_type(+No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `-` is not supported for type ``" reveal_type(-No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `~` is not supported for type ``" reveal_type(~No) # revealed: Unknown ``` @@ -80,11 +80,11 @@ reveal_type(~No) # revealed: Unknown def f(): pass -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `def f() -> Unknown`" +# error: [unsupported-operator] "Unary operator `+` is not supported for type `def f() -> Unknown`" reveal_type(+f) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `def f() -> Unknown`" +# error: [unsupported-operator] "Unary operator `-` is not supported for type `def f() -> Unknown`" reveal_type(-f) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `def f() -> Unknown`" +# error: [unsupported-operator] "Unary operator `~` is not supported for type `def f() -> Unknown`" reveal_type(~f) # revealed: Unknown ``` @@ -113,25 +113,25 @@ def sub() -> type[Sub]: def no() -> type[No]: return No -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Yes]`" +# error: [unsupported-operator] "Unary operator `+` is not supported for type `type[Yes]`" reveal_type(+yes()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Yes]`" +# error: [unsupported-operator] "Unary operator `-` is not supported for type `type[Yes]`" reveal_type(-yes()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Yes]`" +# error: [unsupported-operator] "Unary operator `~` is not supported for type `type[Yes]`" reveal_type(~yes()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Sub]`" +# error: [unsupported-operator] "Unary operator `+` is not supported for type `type[Sub]`" reveal_type(+sub()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Sub]`" +# error: [unsupported-operator] "Unary operator `-` is not supported for type `type[Sub]`" reveal_type(-sub()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Sub]`" +# error: [unsupported-operator] "Unary operator `~` is not supported for type `type[Sub]`" reveal_type(~sub()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[No]`" +# error: [unsupported-operator] "Unary operator `+` is not supported for type `type[No]`" reveal_type(+no()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[No]`" +# error: [unsupported-operator] "Unary operator `-` is not supported for type `type[No]`" reveal_type(-no()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[No]`" +# error: [unsupported-operator] "Unary operator `~` is not supported for type `type[No]`" reveal_type(~no()) # revealed: Unknown ``` @@ -160,10 +160,10 @@ reveal_type(+Sub) # revealed: bool reveal_type(-Sub) # revealed: str reveal_type(~Sub) # revealed: int -# error: [unsupported-operator] "Unary operator `+` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `+` is not supported for type ``" reveal_type(+No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `-` is not supported for type ``" reveal_type(-No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type ``" +# error: [unsupported-operator] "Unary operator `~` is not supported for type ``" reveal_type(~No) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md b/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md index 6b07cb1d2e..100176e1cb 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md @@ -27,7 +27,7 @@ reveal_type(~a) # revealed: Literal[True] class NoDunder: ... b = NoDunder() -+b # error: [unsupported-operator] "Unary operator `+` is unsupported for type `NoDunder`" --b # error: [unsupported-operator] "Unary operator `-` is unsupported for type `NoDunder`" -~b # error: [unsupported-operator] "Unary operator `~` is unsupported for type `NoDunder`" ++b # error: [unsupported-operator] "Unary operator `+` is not supported for type `NoDunder`" +-b # error: [unsupported-operator] "Unary operator `-` is not supported for type `NoDunder`" +~b # error: [unsupported-operator] "Unary operator `~` is not supported for type `NoDunder`" ``` diff --git a/crates/ty_python_semantic/resources/mdtest/unary/not.md b/crates/ty_python_semantic/resources/mdtest/unary/not.md index e01796a9f7..e0cb63d2b5 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/not.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/not.md @@ -187,7 +187,7 @@ class MethodBoolInvalid: def __bool__(self) -> int: return 0 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MethodBoolInvalid`" +# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `MethodBoolInvalid`" # revealed: bool reveal_type(not MethodBoolInvalid()) diff --git a/crates/ty_python_semantic/src/module_resolver/resolver.rs b/crates/ty_python_semantic/src/module_resolver/resolver.rs index 123b4ac31e..60b565a564 100644 --- a/crates/ty_python_semantic/src/module_resolver/resolver.rs +++ b/crates/ty_python_semantic/src/module_resolver/resolver.rs @@ -265,7 +265,14 @@ fn desperately_resolve_module<'db>( let _span = tracing::trace_span!("desperately_resolve_module", %name).entered(); let Some(resolved) = desperately_resolve_name(db, importing_file, name, mode) else { - tracing::debug!("Module `{name}` not found while looking in parent dirs"); + let extra = match module_name.mode(db) { + ModuleResolveMode::StubsAllowed => "neither stub nor real module file", + ModuleResolveMode::StubsNotAllowed => "stubs not allowed", + ModuleResolveMode::StubsNotAllowedSomeShadowingAllowed => { + "stubs not allowed but some shadowing allowed" + } + }; + tracing::debug!("Module `{name}` not found while looking in parent dirs ({extra})"); return None; }; diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 98f7a2b8e4..a1319a3250 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -783,7 +783,7 @@ pub(crate) fn place_by_id<'db>( let declarations = match considered_definitions { ConsideredDefinitions::EndOfScope => use_def.end_of_scope_declarations(place_id), - ConsideredDefinitions::AllReachable => use_def.all_reachable_declarations(place_id), + ConsideredDefinitions::AllReachable => use_def.reachable_declarations(place_id), }; let declared = place_from_declarations_impl(db, declarations, requires_explicit_reexport) @@ -791,7 +791,7 @@ pub(crate) fn place_by_id<'db>( let all_considered_bindings = || match considered_definitions { ConsideredDefinitions::EndOfScope => use_def.end_of_scope_bindings(place_id), - ConsideredDefinitions::AllReachable => use_def.all_reachable_bindings(place_id), + ConsideredDefinitions::AllReachable => use_def.reachable_bindings(place_id), }; // If a symbol is undeclared, but qualified with `typing.Final`, we use the right-hand side diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index f4ab765f08..a38b0a7ded 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -113,10 +113,7 @@ pub(crate) fn attribute_assignments<'db, 's>( let place_table = index.place_table(function_scope_id); let member = place_table.member_id_by_instance_attribute_name(name)?; let use_def = &index.use_def_maps[function_scope_id]; - Some(( - use_def.all_reachable_member_bindings(member), - function_scope_id, - )) + Some((use_def.reachable_member_bindings(member), function_scope_id)) }) } @@ -138,7 +135,7 @@ pub(crate) fn attribute_declarations<'db, 's>( let member = place_table.member_id_by_instance_attribute_name(name)?; let use_def = &index.use_def_maps[function_scope_id]; Some(( - use_def.all_reachable_member_declarations(member), + use_def.reachable_member_declarations(member), function_scope_id, )) }) diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index a7c7520806..dbd26595fd 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -453,17 +453,17 @@ impl<'db> UseDefMap<'db> { ) } - pub(crate) fn all_reachable_bindings( + pub(crate) fn reachable_bindings( &self, place: ScopedPlaceId, ) -> BindingWithConstraintsIterator<'_, 'db> { match place { - ScopedPlaceId::Symbol(symbol) => self.all_reachable_symbol_bindings(symbol), - ScopedPlaceId::Member(member) => self.all_reachable_member_bindings(member), + ScopedPlaceId::Symbol(symbol) => self.reachable_symbol_bindings(symbol), + ScopedPlaceId::Member(member) => self.reachable_member_bindings(member), } } - pub(crate) fn all_reachable_symbol_bindings( + pub(crate) fn reachable_symbol_bindings( &self, symbol: ScopedSymbolId, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -471,7 +471,7 @@ impl<'db> UseDefMap<'db> { self.bindings_iterator(bindings, BoundnessAnalysis::AssumeBound) } - pub(crate) fn all_reachable_member_bindings( + pub(crate) fn reachable_member_bindings( &self, symbol: ScopedMemberId, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -547,7 +547,7 @@ impl<'db> UseDefMap<'db> { self.declarations_iterator(declarations, BoundnessAnalysis::BasedOnUnboundVisibility) } - pub(crate) fn all_reachable_symbol_declarations( + pub(crate) fn reachable_symbol_declarations( &self, symbol: ScopedSymbolId, ) -> DeclarationsIterator<'_, 'db> { @@ -555,7 +555,7 @@ impl<'db> UseDefMap<'db> { self.declarations_iterator(declarations, BoundnessAnalysis::AssumeBound) } - pub(crate) fn all_reachable_member_declarations( + pub(crate) fn reachable_member_declarations( &self, member: ScopedMemberId, ) -> DeclarationsIterator<'_, 'db> { @@ -563,13 +563,13 @@ impl<'db> UseDefMap<'db> { self.declarations_iterator(declarations, BoundnessAnalysis::AssumeBound) } - pub(crate) fn all_reachable_declarations( + pub(crate) fn reachable_declarations( &self, place: ScopedPlaceId, ) -> DeclarationsIterator<'_, 'db> { match place { - ScopedPlaceId::Symbol(symbol) => self.all_reachable_symbol_declarations(symbol), - ScopedPlaceId::Member(member) => self.all_reachable_member_declarations(member), + ScopedPlaceId::Symbol(symbol) => self.reachable_symbol_declarations(symbol), + ScopedPlaceId::Member(member) => self.reachable_member_declarations(member), } } @@ -590,6 +590,30 @@ impl<'db> UseDefMap<'db> { .map(|symbol_id| (symbol_id, self.end_of_scope_symbol_bindings(symbol_id))) } + pub(crate) fn all_reachable_symbols<'map>( + &'map self, + ) -> impl Iterator< + Item = ( + ScopedSymbolId, + DeclarationsIterator<'map, 'db>, + BindingWithConstraintsIterator<'map, 'db>, + ), + > + 'map { + self.reachable_definitions_by_symbol.iter_enumerated().map( + |(symbol_id, reachable_definitions)| { + let declarations = self.declarations_iterator( + &reachable_definitions.declarations, + BoundnessAnalysis::AssumeBound, + ); + let bindings = self.bindings_iterator( + &reachable_definitions.bindings, + BoundnessAnalysis::AssumeBound, + ); + (symbol_id, declarations, bindings) + }, + ) + } + /// This function is intended to be called only once inside `TypeInferenceBuilder::infer_function_body`. pub(crate) fn can_implicitly_return_none(&self, db: &dyn crate::Db) -> bool { !self diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 4dc8a59bab..fb3895a0e0 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -11,7 +11,7 @@ use crate::module_resolver::{KnownModule, Module, list_modules, resolve_module}; use crate::semantic_index::definition::Definition; use crate::semantic_index::scope::FileScopeId; use crate::semantic_index::semantic_index; -use crate::types::list_members::{Member, all_members, all_members_of_scope}; +use crate::types::list_members::{Member, all_members, all_reachable_members}; use crate::types::{Type, binding_type, infer_scope_types}; use crate::{Db, resolve_real_shadowable_module}; @@ -76,7 +76,7 @@ impl<'db> SemanticModel<'db> { for (file_scope, _) in index.ancestor_scopes(file_scope) { for memberdef in - all_members_of_scope(self.db, file_scope.to_scope_id(self.db, self.file)) + all_reachable_members(self.db, file_scope.to_scope_id(self.db, self.file)) { members.insert( memberdef.member.name, @@ -196,7 +196,10 @@ impl<'db> SemanticModel<'db> { /// Returns completions for symbols available in a `object.` context. pub fn attribute_completions(&self, node: &ast::ExprAttribute) -> Vec> { - let ty = node.value.inferred_type(self); + let Some(ty) = node.value.inferred_type(self) else { + return Vec::new(); + }; + all_members(self.db, ty) .into_iter() .map(|member| Completion { @@ -221,7 +224,7 @@ impl<'db> SemanticModel<'db> { let mut completions = vec![]; for (file_scope, _) in index.ancestor_scopes(file_scope) { completions.extend( - all_members_of_scope(self.db, file_scope.to_scope_id(self.db, self.file)).map( + all_reachable_members(self.db, file_scope.to_scope_id(self.db, self.file)).map( |memberdef| Completion { name: memberdef.member.name, ty: Some(memberdef.member.ty), @@ -236,11 +239,45 @@ impl<'db> SemanticModel<'db> { completions } - /// Get the scope of the given node (handles string annotations) + /// Returns the scope in which `node` is defined (handles string annotations). pub fn scope(&self, node: ast::AnyNodeRef<'_>) -> Option { let index = semantic_index(self.db, self.file); match self.node_in_ast(node) { ast::AnyNodeRef::Identifier(identifier) => index.try_expression_scope_id(identifier), + + // Nodes implementing `HasDefinition` + ast::AnyNodeRef::StmtFunctionDef(function) => Some( + function + .definition(self) + .scope(self.db) + .file_scope_id(self.db), + ), + ast::AnyNodeRef::StmtClassDef(class) => { + Some(class.definition(self).scope(self.db).file_scope_id(self.db)) + } + ast::AnyNodeRef::Parameter(parameter) => Some( + parameter + .definition(self) + .scope(self.db) + .file_scope_id(self.db), + ), + ast::AnyNodeRef::ParameterWithDefault(parameter) => Some( + parameter + .definition(self) + .scope(self.db) + .file_scope_id(self.db), + ), + ast::AnyNodeRef::ExceptHandlerExceptHandler(handler) => Some( + handler + .definition(self) + .scope(self.db) + .file_scope_id(self.db), + ), + ast::AnyNodeRef::TypeParamTypeVar(var) => { + Some(var.definition(self).scope(self.db).file_scope_id(self.db)) + } + + // Fallback node => match node.as_expr_ref() { // If we couldn't identify a specific // expression that we're in, then just @@ -400,7 +437,7 @@ pub trait HasType { /// /// ## Panics /// May panic if `self` is from another file than `model`. - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db>; + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option>; } pub trait HasDefinition { @@ -412,18 +449,16 @@ pub trait HasDefinition { } impl HasType for ast::ExprRef<'_> { - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { let index = semantic_index(model.db, model.file); // TODO(#1637): semantic tokens is making this crash even with // `try_expr_ref_in_ast` guarding this, for now just use `try_expression_scope_id`. // The problematic input is `x: "float` (with a dangling quote). I imagine the issue // is we're too eagerly setting `is_string_annotation` in inference. - let Some(file_scope) = index.try_expression_scope_id(&model.expr_ref_in_ast(*self)) else { - return Type::unknown(); - }; + let file_scope = index.try_expression_scope_id(&model.expr_ref_in_ast(*self))?; let scope = file_scope.to_scope_id(model.db, model.file); - infer_scope_types(model.db, scope).expression_type(*self) + infer_scope_types(model.db, scope).try_expression_type(*self) } } @@ -431,7 +466,7 @@ macro_rules! impl_expression_has_type { ($ty: ty) => { impl HasType for $ty { #[inline] - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { let expression_ref = ExprRef::from(self); expression_ref.inferred_type(model) } @@ -474,7 +509,7 @@ impl_expression_has_type!(ast::ExprSlice); impl_expression_has_type!(ast::ExprIpyEscapeCommand); impl HasType for ast::Expr { - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { match self { Expr::BoolOp(inner) => inner.inferred_type(model), Expr::Named(inner) => inner.inferred_type(model), @@ -525,9 +560,9 @@ macro_rules! impl_binding_has_ty_def { impl HasType for $ty { #[inline] - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { let binding = HasDefinition::definition(self, model); - binding_type(model.db, binding) + Some(binding_type(model.db, binding)) } } }; @@ -541,12 +576,12 @@ impl_binding_has_ty_def!(ast::ExceptHandlerExceptHandler); impl_binding_has_ty_def!(ast::TypeParamTypeVar); impl HasType for ast::Alias { - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { if &self.name == "*" { - return Type::Never; + return Some(Type::Never); } let index = semantic_index(model.db, model.file); - binding_type(model.db, index.expect_single_definition(self)) + Some(binding_type(model.db, index.expect_single_definition(self))) } } @@ -584,7 +619,7 @@ mod tests { let function = ast.suite()[0].as_function_def_stmt().unwrap(); let model = SemanticModel::new(&db, foo); - let ty = function.inferred_type(&model); + let ty = function.inferred_type(&model).unwrap(); assert!(ty.is_function_literal()); @@ -603,7 +638,7 @@ mod tests { let class = ast.suite()[0].as_class_def_stmt().unwrap(); let model = SemanticModel::new(&db, foo); - let ty = class.inferred_type(&model); + let ty = class.inferred_type(&model).unwrap(); assert!(ty.is_class_literal()); @@ -624,7 +659,7 @@ mod tests { let import = ast.suite()[0].as_import_from_stmt().unwrap(); let alias = &import.names[0]; let model = SemanticModel::new(&db, bar); - let ty = alias.inferred_type(&model); + let ty = alias.inferred_type(&model).unwrap(); assert!(ty.is_class_literal()); diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 1ecb4504b6..f531fb604e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -878,7 +878,7 @@ impl<'db> Type<'db> { Self::Dynamic(DynamicType::Any) } - pub(crate) const fn unknown() -> Self { + pub const fn unknown() -> Self { Self::Dynamic(DynamicType::Unknown) } @@ -912,8 +912,19 @@ impl<'db> Type<'db> { previous: Self, cycle: &salsa::Cycle, ) -> Self { - UnionType::from_elements_cycle_recovery(db, [self, previous]) - .recursive_type_normalized(db, cycle) + // Avoid unioning two generic aliases of the same class together; this union will never + // simplify and is likely to cause downstream problems. This introduces the theoretical + // possibility of cycle oscillation involving such types (because we are not strictly + // widening the type on each iteration), but so far we have not seen an example of that. + match (previous, self) { + (Type::GenericAlias(prev_alias), Type::GenericAlias(curr_alias)) + if prev_alias.origin(db) == curr_alias.origin(db) => + { + self + } + _ => UnionType::from_elements_cycle_recovery(db, [self, previous]), + } + .recursive_type_normalized(db, cycle) } fn is_none(&self, db: &'db dyn Db) -> bool { @@ -11574,7 +11585,7 @@ impl<'db> BoolError<'db> { not_boolable_type, .. } => { let mut diag = builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for type `{}`", + "Boolean conversion is not supported for type `{}`", not_boolable_type.display(context.db()) )); let mut sub = SubDiagnostic::new( @@ -11599,7 +11610,7 @@ impl<'db> BoolError<'db> { return_type, } => { let mut diag = builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for type `{not_boolable}`", + "Boolean conversion is not supported for type `{not_boolable}`", not_boolable = not_boolable_type.display(context.db()), )); let mut sub = SubDiagnostic::new( @@ -11625,7 +11636,7 @@ impl<'db> BoolError<'db> { } Self::NotCallable { not_boolable_type } => { let mut diag = builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for type `{}`", + "Boolean conversion is not supported for type `{}`", not_boolable_type.display(context.db()) )); let sub = SubDiagnostic::new( @@ -11648,7 +11659,7 @@ impl<'db> BoolError<'db> { .unwrap(); builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for union `{}` \ + "Boolean conversion is not supported for union `{}` \ because `{}` doesn't implement `__bool__` correctly", Type::Union(*union).display(context.db()), first_error.not_boolable_type().display(context.db()), @@ -11657,7 +11668,7 @@ impl<'db> BoolError<'db> { Self::Other { not_boolable_type } => { builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for type `{}`; \ + "Boolean conversion is not supported for type `{}`; \ it incorrectly implements `__bool__`", not_boolable_type.display(context.db()) )); diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index f3097bb66d..ae377785b8 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::fmt::Display; use itertools::{Either, Itertools}; use ruff_python_ast as ast; @@ -263,6 +264,52 @@ impl<'a, 'db> CallArguments<'a, 'db> { State::Expanding(ExpandingState::Expanded(expanded)) => Expansion::Expanded(expanded), }) } + + pub(super) fn display(&self, db: &'db dyn Db) -> impl Display { + struct DisplayCallArguments<'a, 'db> { + call_arguments: &'a CallArguments<'a, 'db>, + db: &'db dyn Db, + } + + impl std::fmt::Display for DisplayCallArguments<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("(")?; + for (index, (argument, ty)) in self.call_arguments.iter().enumerate() { + if index > 0 { + write!(f, ", ")?; + } + match argument { + Argument::Synthetic => write!( + f, + "self: {}", + ty.unwrap_or_else(Type::unknown).display(self.db) + )?, + Argument::Positional => { + write!(f, "{}", ty.unwrap_or_else(Type::unknown).display(self.db))?; + } + Argument::Variadic => { + write!(f, "*{}", ty.unwrap_or_else(Type::unknown).display(self.db))?; + } + Argument::Keyword(name) => write!( + f, + "{}={}", + name, + ty.unwrap_or_else(Type::unknown).display(self.db) + )?, + Argument::Keywords => { + write!(f, "**{}", ty.unwrap_or_else(Type::unknown).display(self.db))?; + } + } + } + f.write_str(")") + } + } + + DisplayCallArguments { + call_arguments: self, + db, + } + } } /// Represents a single element of the expansion process for argument types for [`expand`]. diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 29b176ec8a..e81d26d8b8 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1603,10 +1603,16 @@ impl<'db> CallableBinding<'db> { // before checking. let argument_types = argument_types.with_self(self.bound_type); - tracing::debug!( + let _span = tracing::trace_span!( + "CallableBinding::check_types", + arguments = %argument_types.display(db), + signature = %self.signature_type.display(db), + ) + .entered(); + + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 1", ); @@ -1640,10 +1646,9 @@ impl<'db> CallableBinding<'db> { overload.check_types(db, argument_types.as_ref(), call_expression_tcx); } - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 2", ); @@ -1659,10 +1664,9 @@ impl<'db> CallableBinding<'db> { // If two or more candidate overloads remain, proceed to step 4. self.filter_overloads_containing_variadic(&indexes); - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 4", ); @@ -1685,10 +1689,9 @@ impl<'db> CallableBinding<'db> { &indexes, ); - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 5", ); } @@ -1793,10 +1796,9 @@ impl<'db> CallableBinding<'db> { overload.match_parameters(db, expanded_arguments, &mut argument_forms); } - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 1", ); @@ -1806,10 +1808,9 @@ impl<'db> CallableBinding<'db> { overload.check_types(db, expanded_arguments, call_expression_tcx); } - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 2", ); @@ -1821,10 +1822,9 @@ impl<'db> CallableBinding<'db> { MatchingOverloadIndex::Multiple(matching_overload_indexes) => { self.filter_overloads_containing_variadic(&matching_overload_indexes); - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 4", ); @@ -1843,10 +1843,9 @@ impl<'db> CallableBinding<'db> { &indexes, ); - tracing::debug!( + tracing::trace!( target: "ty_python_semantic::types::call::bind", matching_overload_index = ?self.matching_overload_index(), - signature = %self.signature_type.display(db), "after step 5", ); diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 71ae73f1b1..32962ea128 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -3457,7 +3457,7 @@ impl<'db> ClassLiteral<'db> { .symbol_id(&method_def.node(&module).name) .unwrap(); class_map - .all_reachable_symbol_bindings(method_place) + .reachable_symbol_bindings(method_place) .find_map(|bind| { (bind.binding.is_defined_and(|def| def == method)) .then(|| class_map.binding_reachability(db, &bind)) diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index 9a91d1da3d..fcc8552b52 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -76,7 +76,7 @@ pub(crate) fn enum_metadata<'db>( let mut auto_counter = 0; let ignored_names: Option> = if let Some(ignore) = table.symbol_id("_ignore_") { - let ignore_bindings = use_def_map.all_reachable_symbol_bindings(ignore); + let ignore_bindings = use_def_map.reachable_symbol_bindings(ignore); let ignore_place = place_from_bindings(db, ignore_bindings).place; match ignore_place { diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index d32eba2ace..4087d125c6 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -86,9 +86,9 @@ pub fn definitions_for_name<'db>( if let Some(global_symbol_id) = global_place_table.symbol_id(name_str) { let global_use_def_map = crate::semantic_index::use_def_map(db, global_scope_id); let global_bindings = - global_use_def_map.all_reachable_symbol_bindings(global_symbol_id); + global_use_def_map.reachable_symbol_bindings(global_symbol_id); let global_declarations = - global_use_def_map.all_reachable_symbol_declarations(global_symbol_id); + global_use_def_map.reachable_symbol_declarations(global_symbol_id); for binding in global_bindings { if let Some(def) = binding.binding.definition() { @@ -114,8 +114,8 @@ pub fn definitions_for_name<'db>( let use_def_map = index.use_def_map(scope_id); // Get all definitions (both bindings and declarations) for this place - let bindings = use_def_map.all_reachable_symbol_bindings(symbol_id); - let declarations = use_def_map.all_reachable_symbol_declarations(symbol_id); + let bindings = use_def_map.reachable_symbol_bindings(symbol_id); + let declarations = use_def_map.reachable_symbol_declarations(symbol_id); for binding in bindings { if let Some(def) = binding.binding.definition() { @@ -155,7 +155,8 @@ pub fn definitions_for_name<'db>( // https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex if matches!(name_str, "float" | "complex") && let Some(expr) = node.expr_name() - && let Some(union) = expr.inferred_type(&SemanticModel::new(db, file)).as_union() + && let Some(ty) = expr.inferred_type(model) + && let Some(union) = ty.as_union() && is_float_or_complex_annotation(db, union, name_str) { return union @@ -234,7 +235,10 @@ pub fn definitions_for_attribute<'db>( let mut resolved = Vec::new(); // Determine the type of the LHS - let lhs_ty = attribute.value.inferred_type(model); + let Some(lhs_ty) = attribute.value.inferred_type(model) else { + return resolved; + }; + let tys = match lhs_ty { Type::Union(union) => union.elements(model.db()).to_vec(), _ => vec![lhs_ty], @@ -294,7 +298,7 @@ pub fn definitions_for_attribute<'db>( let use_def = use_def_map(db, class_scope); // Check declarations first - for decl in use_def.all_reachable_symbol_declarations(place_id) { + for decl in use_def.reachable_symbol_declarations(place_id) { if let Some(def) = decl.declaration.definition() { resolved.extend(resolve_definition( db, @@ -307,7 +311,7 @@ pub fn definitions_for_attribute<'db>( } // If no declarations found, check bindings - for binding in use_def.all_reachable_symbol_bindings(place_id) { + for binding in use_def.reachable_symbol_bindings(place_id) { if let Some(def) = binding.binding.definition() { resolved.extend(resolve_definition( db, @@ -332,7 +336,7 @@ pub fn definitions_for_attribute<'db>( let use_def = index.use_def_map(function_scope_id); // Check declarations first - for decl in use_def.all_reachable_member_declarations(place_id) { + for decl in use_def.reachable_member_declarations(place_id) { if let Some(def) = decl.declaration.definition() { resolved.extend(resolve_definition( db, @@ -345,7 +349,7 @@ pub fn definitions_for_attribute<'db>( } // If no declarations found, check bindings - for binding in use_def.all_reachable_member_bindings(place_id) { + for binding in use_def.reachable_member_bindings(place_id) { if let Some(def) = binding.binding.definition() { resolved.extend(resolve_definition( db, @@ -374,7 +378,9 @@ pub fn definitions_for_keyword_argument<'db>( call_expr: &ast::ExprCall, ) -> Vec> { let db = model.db(); - let func_type = call_expr.func.inferred_type(model); + let Some(func_type) = call_expr.func.inferred_type(model) else { + return Vec::new(); + }; let Some(keyword_name) = keyword.arg.as_ref() else { return Vec::new(); @@ -498,7 +504,9 @@ pub fn call_signature_details<'db>( model: &SemanticModel<'db>, call_expr: &ast::ExprCall, ) -> Vec> { - let func_type = call_expr.func.inferred_type(model); + let Some(func_type) = call_expr.func.inferred_type(model) else { + return Vec::new(); + }; // Use into_callable to handle all the complex type conversions if let Some(callable_type) = func_type @@ -507,7 +515,9 @@ pub fn call_signature_details<'db>( { let call_arguments = CallArguments::from_arguments(&call_expr.arguments, |_, splatted_value| { - splatted_value.inferred_type(model) + splatted_value + .inferred_type(model) + .unwrap_or(Type::unknown()) }); let bindings = callable_type .bindings(model.db()) @@ -564,7 +574,7 @@ pub fn call_type_simplified_by_overloads( call_expr: &ast::ExprCall, ) -> Option { let db = model.db(); - let func_type = call_expr.func.inferred_type(model); + let func_type = call_expr.func.inferred_type(model)?; // Use into_callable to handle all the complex type conversions let callable_type = func_type.try_upcast_to_callable(db)?.into_type(db); @@ -579,7 +589,9 @@ pub fn call_type_simplified_by_overloads( // Hand the overload resolution system as much type info as we have let args = CallArguments::from_arguments_typed(&call_expr.arguments, |_, splatted_value| { - splatted_value.inferred_type(model) + splatted_value + .inferred_type(model) + .unwrap_or(Type::unknown()) }); // Try to resolve overloads with the arguments/types we have @@ -612,8 +624,8 @@ pub fn definitions_for_bin_op<'db>( model: &SemanticModel<'db>, binary_op: &ast::ExprBinOp, ) -> Option<(Vec>, Type<'db>)> { - let left_ty = binary_op.left.inferred_type(model); - let right_ty = binary_op.right.inferred_type(model); + let left_ty = binary_op.left.inferred_type(model)?; + let right_ty = binary_op.right.inferred_type(model)?; let Ok(bindings) = Type::try_call_bin_op(model.db(), left_ty, binary_op.op, right_ty) else { return None; @@ -639,7 +651,7 @@ pub fn definitions_for_unary_op<'db>( model: &SemanticModel<'db>, unary_op: &ast::ExprUnaryOp, ) -> Option<(Vec>, Type<'db>)> { - let operand_ty = unary_op.operand.inferred_type(model); + let operand_ty = unary_op.operand.inferred_type(model)?; let unary_dunder_method = match unary_op.op { ast::UnaryOp::Invert => "__invert__", @@ -843,6 +855,7 @@ mod resolve_definition { use ruff_db::system::SystemPath; use ruff_db::vendored::VendoredPathBuf; use ruff_python_ast as ast; + use ruff_python_stdlib::sys::is_builtin_module; use rustc_hash::FxHashSet; use tracing::trace; @@ -1096,8 +1109,8 @@ mod resolve_definition { let mut definitions = IndexSet::new(); // Get all definitions (both bindings and declarations) for this place - let bindings = use_def_map.all_reachable_symbol_bindings(symbol_id); - let declarations = use_def_map.all_reachable_symbol_declarations(symbol_id); + let bindings = use_def_map.reachable_symbol_bindings(symbol_id); + let declarations = use_def_map.reachable_symbol_declarations(symbol_id); for binding in bindings { if let Some(def) = binding.binding.definition() { @@ -1160,6 +1173,14 @@ mod resolve_definition { // here because there isn't really an importing file. However this `resolve_real_module` // can be understood as essentially `import .`, which is also what `file_to_module` is, // so this is in fact exactly the file we want to consider the importer. + // + // ... unless we have a builtin module. i.e., A module embedded + // into the interpreter. In which case, all we have are stubs. + // `resolve_real_module` will always return `None` for this case, but + // it will emit false positive logs. And this saves us some work. + if is_builtin_module(db.python_version().minor, stub_module.name(db)) { + return None; + } let real_module = resolve_real_module(db, stub_file_for_module_lookup, stub_module.name(db))?; trace!("Found real module: {}", real_module.name(db)); diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 14e3409874..00141d4ca8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -5916,7 +5916,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return; }; builder.into_diagnostic(format_args!( - "Operator `{op}=` is unsupported between objects of type `{}` and `{}`", + "Operator `{op}=` is not supported between objects of type `{}` and `{}`", target_type.display(db), value_type.display(db) )); @@ -8900,7 +8900,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If we're inferring types of deferred expressions, look them up from end-of-scope. if self.is_deferred() { let place = if let Some(place_id) = place_table.place_id(expr) { - place_from_bindings(db, use_def.all_reachable_bindings(place_id)).place + place_from_bindings(db, use_def.reachable_bindings(place_id)).place } else { assert!( self.deferred_state.in_string_annotation(), @@ -9685,7 +9685,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.report_lint(&UNSUPPORTED_OPERATOR, unary) { builder.into_diagnostic(format_args!( - "Unary operator `{op}` is unsupported for type `{}`", + "Unary operator `{op}` is not supported for type `{}`", operand_type.display(self.db()), )); } @@ -9722,7 +9722,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(builder) = self.context.report_lint(&UNSUPPORTED_OPERATOR, binary) { let mut diag = builder.into_diagnostic(format_args!( - "Operator `{op}` is unsupported between objects of type `{}` and `{}`", + "Operator `{op}` is not supported between objects of type `{}` and `{}`", left_ty.display(db), right_ty.display(db) )); diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index fbae2c8948..626225cdc8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -155,7 +155,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } // anything else is an invalid annotation: op => { - self.infer_binary_expression(binary, TypeContext::default()); + // Avoid inferring the types of invalid binary expressions that have been + // parsed from a string annotation, as they are not present in the semantic + // index. + if !self.deferred_state.in_string_annotation() { + self.infer_binary_expression(binary, TypeContext::default()); + } self.report_invalid_type_expression( expression, format_args!( diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index eb47745b74..a93438a596 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -25,8 +25,9 @@ use crate::{ }, }; -/// Iterate over all declarations and bindings in the given scope. -pub(crate) fn all_members_of_scope<'db>( +/// Iterate over all declarations and bindings that exist at the end +/// of the given scope. +pub(crate) fn all_end_of_scope_members<'db>( db: &'db dyn Db, scope_id: ScopeId<'db>, ) -> impl Iterator> + 'db { @@ -75,6 +76,60 @@ pub(crate) fn all_members_of_scope<'db>( )) } +/// Iterate over all declarations and bindings that are reachable anywhere +/// in the given scope. +pub(crate) fn all_reachable_members<'db>( + db: &'db dyn Db, + scope_id: ScopeId<'db>, +) -> impl Iterator> + 'db { + let use_def_map = use_def_map(db, scope_id); + let table = place_table(db, scope_id); + + use_def_map + .all_reachable_symbols() + .flat_map(move |(symbol_id, declarations, bindings)| { + let symbol = table.symbol(symbol_id); + + let declaration_place_result = place_from_declarations(db, declarations); + let declaration = + declaration_place_result + .first_declaration + .and_then(|first_reachable_definition| { + let ty = declaration_place_result + .ignore_conflicting_declarations() + .place + .ignore_possibly_undefined()?; + let member = Member { + name: symbol.name().clone(), + ty, + }; + Some(MemberWithDefinition { + member, + first_reachable_definition, + }) + }); + + let place_with_definition = place_from_bindings(db, bindings); + let binding = + place_with_definition + .first_definition + .and_then(|first_reachable_definition| { + let ty = place_with_definition.place.ignore_possibly_undefined()?; + let member = Member { + name: symbol.name().clone(), + ty, + }; + Some(MemberWithDefinition { + member, + first_reachable_definition, + }) + }); + + [declaration, binding] + }) + .flatten() +} + // `__init__`, `__repr__`, `__eq__`, `__ne__` and `__hash__` are always included via `object`, // so we don't need to list them here. const SYNTHETIC_DATACLASS_ATTRIBUTES: &[&str] = &[ @@ -359,7 +414,7 @@ impl<'db> AllMembers<'db> { .map(|class| class.class_literal(db).0) { let parent_scope = parent.body_scope(db); - for memberdef in all_members_of_scope(db, parent_scope) { + for memberdef in all_end_of_scope_members(db, parent_scope) { let result = ty.member(db, memberdef.member.name.as_str()); let Some(ty) = result.place.ignore_possibly_undefined() else { continue; @@ -407,7 +462,7 @@ impl<'db> AllMembers<'db> { // class member. This gets us the right type for each // member, e.g., `SomeClass.__delattr__` is not a bound // method, but `instance_of_SomeClass.__delattr__` is. - for memberdef in all_members_of_scope(db, class_body_scope) { + for memberdef in all_end_of_scope_members(db, class_body_scope) { let result = ty.member(db, memberdef.member.name.as_str()); let Some(ty) = result.place.ignore_possibly_undefined() else { continue; diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index 5d6328a606..c56581358c 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -25,7 +25,7 @@ use crate::{ report_overridden_final_method, }, function::{FunctionDecorators, FunctionType, KnownFunction}, - list_members::{Member, MemberWithDefinition, all_members_of_scope}, + list_members::{Member, MemberWithDefinition, all_end_of_scope_members}, }, }; @@ -54,7 +54,7 @@ pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: ClassLite let class_specialized = class.identity_specialization(db); let scope = class.body_scope(db); - let own_class_members: FxHashSet<_> = all_members_of_scope(db, scope).collect(); + let own_class_members: FxHashSet<_> = all_end_of_scope_members(db, scope).collect(); for member in own_class_members { check_class_declaration(context, configuration, class_specialized, scope, &member); @@ -129,7 +129,7 @@ fn check_class_declaration<'db>( && PROHIBITED_NAMEDTUPLE_ATTRS.contains(&member.name.as_str()) && let Some(symbol_id) = place_table(db, class_scope).symbol_id(&member.name) && let Some(bad_definition) = use_def_map(db, class_scope) - .all_reachable_bindings(ScopedPlaceId::Symbol(symbol_id)) + .reachable_bindings(ScopedPlaceId::Symbol(symbol_id)) .filter_map(|binding| binding.binding.definition()) .find(|def| !matches!(def.kind(db), DefinitionKind::AnnotatedAssignment(_))) && let Some(builder) = context.report_lint( diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index e07fbb999d..dee49a3afe 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -71,7 +71,7 @@ impl<'db> TypedDictType<'db> { } pub(crate) fn items(self, db: &'db dyn Db) -> &'db TypedDictSchema<'db> { - #[salsa::tracked(returns(ref))] + #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] fn class_based_items<'db>(db: &'db dyn Db, class: ClassType<'db>) -> TypedDictSchema<'db> { let (class_literal, specialization) = class.class_literal(db); class_literal