[ty_test] Calculate hover column at parse time

Move the column calculation from generate_hover_outputs to the
HoverAssertion parsing logic. This makes better use of the existing
column field in HoverAssertion.

Changes:
- UnparsedAssertion::Hover now stores both the expected type and the
  full comment text
- HoverAssertion::from_str() now takes both parameters and calculates
  the column from the down arrow position in the full comment
- generate_hover_outputs() now reads the column from the parsed
  assertion instead of recalculating it

This eliminates redundant calculations and makes the column field
actually useful.

🤖 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 14:09:00 -04:00
parent 0198857224
commit 51d5bc709d
2 changed files with 37 additions and 21 deletions

View File

@ -246,23 +246,26 @@ pub(crate) enum UnparsedAssertion<'a> {
/// An `# error:` assertion.
Error(&'a str),
/// A `# hover:` assertion, with the full comment text including the down arrow.
Hover(&'a str),
/// A `# hover:` assertion.
///
/// The first string is the expected type (body after `hover:`).
/// The second string is the full comment text (including the down arrow).
Hover(&'a str, &'a str),
}
impl<'a> UnparsedAssertion<'a> {
/// Returns `Some(_)` if the comment starts with `# error:`, `# revealed:`, or `# hover:`,
/// indicating that it is an assertion comment.
fn from_comment(comment: &'a str) -> Option<Self> {
let comment = comment.trim().strip_prefix('#')?.trim();
let (keyword, body) = comment.split_once(':')?;
let trimmed = comment.trim().strip_prefix('#')?.trim();
let (keyword, body) = trimmed.split_once(':')?;
let keyword = keyword.trim();
let body = body.trim();
match keyword {
"revealed" => Some(Self::Revealed(body)),
"error" => Some(Self::Error(body)),
"hover" | "↓ hover" => Some(Self::Hover(body)),
"hover" | "↓ hover" => Some(Self::Hover(body, comment)),
_ => None,
}
}
@ -280,9 +283,11 @@ impl<'a> UnparsedAssertion<'a> {
Self::Error(error) => ErrorAssertion::from_str(error)
.map(ParsedAssertion::Error)
.map_err(PragmaParseError::ErrorAssertionParseError),
Self::Hover(hover) => HoverAssertion::from_str(hover)
.map(ParsedAssertion::Hover)
.map_err(PragmaParseError::HoverAssertionParseError),
Self::Hover(expected_type, full_comment) => {
HoverAssertion::from_str(expected_type, full_comment)
.map(ParsedAssertion::Hover)
.map_err(PragmaParseError::HoverAssertionParseError)
}
}
}
}
@ -292,7 +297,7 @@ impl std::fmt::Display for UnparsedAssertion<'_> {
match self {
Self::Revealed(expected_type) => write!(f, "revealed: {expected_type}"),
Self::Error(assertion) => write!(f, "error: {assertion}"),
Self::Hover(expected_type) => write!(f, "hover: {expected_type}"),
Self::Hover(expected_type, _) => write!(f, "hover: {expected_type}"),
}
}
}
@ -367,16 +372,25 @@ pub(crate) struct HoverAssertion<'a> {
}
impl<'a> HoverAssertion<'a> {
fn from_str(source: &'a str) -> Result<Self, HoverAssertionParseError> {
if source.is_empty() {
fn from_str(
expected_type: &'a str,
full_comment: &'a str,
) -> Result<Self, HoverAssertionParseError> {
if expected_type.is_empty() {
return Err(HoverAssertionParseError::EmptyType);
}
// Column will be computed from the comment position in the matcher
// For now, we just validate and store the expected type
// Find the down arrow position in the full comment to determine the column
let arrow_position = full_comment
.find('↓')
.ok_or(HoverAssertionParseError::MissingDownArrow)?;
// Column is 1-indexed, and the arrow position is 0-indexed
let column = OneIndexed::from_zero_indexed(arrow_position);
Ok(Self {
column: OneIndexed::from_zero_indexed(0), // Placeholder, will be set by matcher
expected_type: source,
column,
expected_type,
})
}
}

View File

@ -95,21 +95,23 @@ pub(crate) fn generate_hover_outputs(
// Look for hover assertions in this line's assertions
for assertion in line_assertions.iter() {
let crate::assertion::UnparsedAssertion::Hover(hover_text) = assertion else {
let crate::assertion::UnparsedAssertion::Hover(_, _) = assertion else {
continue;
};
// Find the down arrow position in the comment text to determine the column
let Some(arrow_position) = hover_text.find('↓') else {
// No down arrow - skip this hover assertion (will be caught as error by matcher)
// Parse the assertion to get the column
let Ok(crate::assertion::ParsedAssertion::Hover(hover)) = assertion.parse() else {
// Invalid hover assertion - will be caught as error by matcher
continue;
};
// 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 (0-indexed)
let hover_offset = target_line_start + TextSize::try_from(arrow_position).unwrap();
// Calculate the hover position from the column in the parsed assertion
// Column is 1-indexed, so convert to 0-indexed for TextSize
let hover_offset =
target_line_start + TextSize::try_from(hover.column.get() - 1).unwrap();
// Get the inferred type at that position
let Some(inferred_type) = infer_type_at_position(db, file, hover_offset) else {