[ty_test] Fix hover assertion line number calculation

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 <noreply@anthropic.com>
This commit is contained in:
Douglas Creager 2025-10-08 13:05:07 -04:00
parent 319f5be78c
commit 19600ecd51
2 changed files with 34 additions and 44 deletions

View File

@ -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,49 +68,33 @@ fn infer_type_at_position(db: &Db, file: File, offset: TextSize) -> Option<Strin
/// Generate hover CheckOutputs for all hover assertions in a file.
///
/// This scans the file for hover assertions (comments with `# ↓ hover:`),
/// computes the hover position from the down arrow location, calls the type
/// inference, and returns CheckOutput::Hover entries.
pub(crate) fn generate_hover_outputs(db: &Db, file: File) -> Vec<matcher::CheckOutput> {
/// 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<matcher::CheckOutput> {
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];
// Check if this is a hover assertion (contains "# ↓ hover:" or "# hover:")
if !comment_text.trim().starts_with('#') {
continue;
}
let trimmed = comment_text.trim().strip_prefix('#').unwrap().trim();
if !trimmed.starts_with("↓ hover:") && !trimmed.starts_with("hover:") {
continue;
}
// 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);
// 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;
// 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);
// Calculate the hover position: start of target line + arrow column
let hover_offset = target_line_start + TextSize::try_from(arrow_column).unwrap();
// Calculate the hover position: start of target line + arrow column (0-indexed)
let hover_offset =
target_line_start + TextSize::try_from(arrow_position).unwrap();
// Get the inferred type at that position
if let Some(inferred_type) = infer_type_at_position(db, file, hover_offset) {
@ -121,6 +104,10 @@ pub(crate) fn generate_hover_outputs(db: &Db, file: File) -> Vec<matcher::CheckO
});
}
}
// If no down arrow, skip this hover assertion (will be caught as error by matcher)
}
}
}
hover_outputs
}

View File

@ -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,