From 19600ecd510ce5b098d8ea9ab5123203b0d85863 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 8 Oct 2025 13:05:07 -0400 Subject: [PATCH] [ty_test] Fix hover assertion line number calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous implementation incorrectly assumed the target line was always line_number + 1, which breaks when multiple assertion comments are stacked on consecutive lines. Now generate_hover_outputs accepts the parsed InlineFileAssertions, which already correctly associates each assertion with its target line number (accounting for stacked comments). For the column position, we extract it directly from the down arrow position in the UnparsedAssertion::Hover text, avoiding the need to parse the assertion ourselves (parsing happens later in the matcher). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- crates/ty_test/src/hover.rs | 73 +++++++++++++++---------------------- crates/ty_test/src/lib.rs | 5 ++- 2 files changed, 34 insertions(+), 44 deletions(-) diff --git a/crates/ty_test/src/hover.rs b/crates/ty_test/src/hover.rs index ef6adf6a5e..0fc0706c2b 100644 --- a/crates/ty_test/src/hover.rs +++ b/crates/ty_test/src/hover.rs @@ -9,7 +9,6 @@ use ruff_db::parsed::parsed_module; use ruff_db::source::{line_index, source_text}; use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal}; use ruff_python_ast::AnyNodeRef; -use ruff_python_trivia::CommentRanges; use ruff_text_size::{Ranged, TextSize}; use ty_python_semantic::{HasType, SemanticModel}; @@ -69,56 +68,44 @@ fn infer_type_at_position(db: &Db, file: File, offset: TextSize) -> Option Vec { +/// Uses the parsed assertions from the assertion module, which correctly handles +/// multiple stacked assertion comments and determines the target line number. +pub(crate) fn generate_hover_outputs( + db: &Db, + file: File, + assertions: &crate::assertion::InlineFileAssertions, +) -> Vec { let source = source_text(db, file); let lines = line_index(db, file); - let parsed = parsed_module(db, file).load(db); - let comment_ranges = CommentRanges::from(parsed.tokens()); let mut hover_outputs = Vec::new(); - for comment_range in &comment_ranges { - let comment_text = &source[comment_range]; + // Iterate through all assertion groups, which are already associated with their target line + for line_assertions in assertions { + let target_line = line_assertions.line_number; - // Check if this is a hover assertion (contains "# ↓ hover:" or "# hover:") - if !comment_text.trim().starts_with('#') { - continue; - } + // Look for hover assertions in this line's assertions + for assertion in line_assertions.iter() { + if let crate::assertion::UnparsedAssertion::Hover(hover_text) = assertion { + // Find the down arrow position in the comment text to determine the column + if let Some(arrow_position) = hover_text.find('↓') { + // Get the start offset of the target line + let target_line_start = lines.line_start(target_line, &source); - let trimmed = comment_text.trim().strip_prefix('#').unwrap().trim(); - if !trimmed.starts_with("↓ hover:") && !trimmed.starts_with("hover:") { - continue; - } + // Calculate the hover position: start of target line + arrow column (0-indexed) + let hover_offset = + target_line_start + TextSize::try_from(arrow_position).unwrap(); - // Find the down arrow position in the comment - let arrow_offset = comment_text.find('↓'); - if arrow_offset.is_none() { - // No down arrow means we can't determine the column - continue; - } - let arrow_column = arrow_offset.unwrap(); - - // Get the line number of the comment - let comment_line = lines.line_index(comment_range.start()); - - // The hover target is the next non-comment, non-empty line - let target_line = comment_line.saturating_add(1); - - // Get the start offset of the target line - let target_line_start = lines.line_start(target_line, &source); - - // Calculate the hover position: start of target line + arrow column - let hover_offset = target_line_start + TextSize::try_from(arrow_column).unwrap(); - - // Get the inferred type at that position - if let Some(inferred_type) = infer_type_at_position(db, file, hover_offset) { - hover_outputs.push(matcher::CheckOutput::Hover { - offset: hover_offset, - inferred_type, - }); + // Get the inferred type at that position + if let Some(inferred_type) = infer_type_at_position(db, file, hover_offset) { + hover_outputs.push(matcher::CheckOutput::Hover { + offset: hover_offset, + inferred_type, + }); + } + } + // If no down arrow, skip this hover assertion (will be caught as error by matcher) + } } } diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index 94ca20eaaa..5d50d06636 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -377,8 +377,11 @@ fn run_test( .map(|diag| matcher::CheckOutput::Diagnostic(diag.clone())) .collect(); + // Parse assertions to get hover assertions with correct line numbers + let assertions = assertion::InlineFileAssertions::from_file(db, test_file.file); + // Generate and add hover outputs - check_outputs.extend(hover::generate_hover_outputs(db, test_file.file)); + check_outputs.extend(hover::generate_hover_outputs(db, test_file.file, &assertions)); let failure = match matcher::match_file(db, test_file.file, &check_outputs) { Ok(()) => None,