mirror of https://github.com/astral-sh/ruff
Add hover assertion type to mdtest framework
- Add Hover variant to UnparsedAssertion and ParsedAssertion enums
- Create HoverAssertion struct with column and expected_type fields
- Add HoverAssertionParseError enum for validation errors
- Update from_comment() to recognize '# hover:' and '# ↓ hover:' patterns
- Simplified design: down arrow must appear immediately before 'hover' keyword
- Add placeholder matching logic in matcher.rs (to be completed)
- Add PLAN.md to track implementation progress
The hover assertion syntax uses a down arrow to indicate column position:
# ↓ hover: expected_type
expression_to_hover
This will enable testing hover functionality in mdtest files, similar to
how ty_ide tests work with <CURSOR> markers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3771f1567c
commit
37effea8fd
|
|
@ -0,0 +1,120 @@
|
|||
# Hover Assertions in Mdtest Framework
|
||||
|
||||
## Goal
|
||||
|
||||
Add support for hover assertions in the mdtest framework. These assertions will verify the inferred type of an expression at a specific position, similar to how hover works in a language server.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
### ty_ide hover tests
|
||||
- Use `<CURSOR>` marker in test source code (hover.rs:161-167)
|
||||
- Call `hover(db, file, offset)` to get type at cursor position (hover.rs:12-50)
|
||||
- Use `find_goto_target()` to find the covering AST node (hover.rs:14)
|
||||
- Return the inferred type via `SemanticModel` (hover.rs:22-23)
|
||||
|
||||
### mdtest framework
|
||||
- Parses markdown files with Python code blocks (parser.rs)
|
||||
- Supports two assertion types via comments (assertion.rs:242-248):
|
||||
- `# error:` - matches diagnostics
|
||||
- `# revealed:` - matches revealed-type diagnostics
|
||||
- Assertions can be:
|
||||
- End-of-line: `x: int = "foo" # error: [invalid-assignment]`
|
||||
- Preceding line: `# error: [invalid-assignment]\nx: int = "foo"`
|
||||
- Matcher logic in matcher.rs compares assertions against diagnostics
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Add new assertion type (assertion.rs)
|
||||
**Status:** ✅ Completed
|
||||
|
||||
- [x] Add `Hover(&'a str)` variant to `UnparsedAssertion` enum (line 242)
|
||||
- [x] Update `from_comment()` to recognize `# hover:` and `# ↓ hover:` (line 253)
|
||||
- [x] Add `Hover(HoverAssertion<'a>)` to `ParsedAssertion` enum (line 294)
|
||||
- [x] Add `HoverAssertion` struct with column and expected_type fields
|
||||
- [x] Add `HoverAssertionParseError` enum
|
||||
- [x] Update parsing logic to validate hover assertions (line 267)
|
||||
|
||||
### 2. Extend assertion parsing to capture column position
|
||||
**Status:** ✅ Simplified approach
|
||||
|
||||
- [x] Hover assertions MUST be preceding-line only (not end-of-line)
|
||||
- [x] Down arrow must appear immediately before `hover` keyword (e.g., `# ↓ hover:`)
|
||||
- [x] Column position determined by whitespace before `#` in comment
|
||||
- [x] Calculate TextSize offset from: (target_line_start + down_arrow_column)
|
||||
|
||||
### 3. Create hover diagnostic type (diagnostic.rs)
|
||||
**Status:** Not started
|
||||
|
||||
- [ ] Add new diagnostic ID for hover results (similar to `RevealedType`)
|
||||
- [ ] Store the position and inferred type as a diagnostic
|
||||
- [ ] This allows reuse of existing matcher infrastructure
|
||||
|
||||
### 4. Add hover checking logic (lib.rs)
|
||||
**Status:** Not started
|
||||
|
||||
- [ ] After type checking, for each hover assertion:
|
||||
- [ ] Find the target line (first non-assertion line after hover comment)
|
||||
- [ ] Calculate position from down arrow column
|
||||
- [ ] Call `hover(db, file, position)` from ty_ide
|
||||
- [ ] Create a "hover diagnostic" with the result
|
||||
- [ ] Add to diagnostics list for matching
|
||||
|
||||
### 5. Update matcher (matcher.rs)
|
||||
**Status:** In progress
|
||||
|
||||
- [x] Add placeholder matching logic for `ParsedAssertion::Hover`
|
||||
- [ ] Compute column position from comment range when matching
|
||||
- [ ] Match against hover diagnostics by:
|
||||
- [ ] Position (must match exactly)
|
||||
- [ ] Type string (must match the assertion body)
|
||||
- [ ] Handle `@Todo` metadata stripping (line 201-212)
|
||||
|
||||
### 6. Add tests
|
||||
**Status:** Not started
|
||||
|
||||
- [ ] Add unit tests for hover assertion parsing
|
||||
- [ ] Create mdtest file with hover assertion examples
|
||||
- [ ] Test edge cases (no hover content, wrong position, etc.)
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **Preceding-line only**: Hover assertions make sense only as preceding-line comments, since we need to identify both line and column via the down arrow.
|
||||
|
||||
2. **Down arrow syntax**: `# ↓ hover: int` where the arrow column identifies the hover position. This is intuitive and visual.
|
||||
|
||||
3. **Reuse diagnostic infrastructure**: By converting hover results to diagnostics, we leverage the existing matcher framework rather than creating parallel logic.
|
||||
|
||||
4. **Similar to revealed assertions**: The implementation will closely mirror the `revealed:` assertion logic, as both check inferred types.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```python
|
||||
# Test basic type inference
|
||||
a = 10
|
||||
# ↓ hover: Literal[10]
|
||||
a
|
||||
|
||||
# Test function type
|
||||
def foo() -> int: ...
|
||||
# ↓ hover: def foo() -> int
|
||||
foo
|
||||
```
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `crates/ty_test/src/assertion.rs` - Add `Hover` assertion type
|
||||
2. `crates/ty_test/src/lib.rs` - Add hover checking logic
|
||||
3. `crates/ty_test/src/matcher.rs` - Add hover matching logic
|
||||
4. `crates/ty_test/src/diagnostic.rs` - Add hover diagnostic type (if needed)
|
||||
5. Tests in existing mdtest files to validate
|
||||
|
||||
## Progress Log
|
||||
|
||||
- **2025-10-08**: Initial plan created based on codebase analysis
|
||||
- **2025-10-08**: Completed step 1 - Added hover assertion type to assertion.rs
|
||||
- Added `Hover` variant to `UnparsedAssertion` and `ParsedAssertion` enums
|
||||
- Created `HoverAssertion` struct and `HoverAssertionParseError` enum
|
||||
- Updated `from_comment()` to recognize `# hover:` and `# ↓ hover:` patterns
|
||||
- Simplified approach: down arrow must appear immediately before `hover` keyword
|
||||
- Added placeholder matching logic in matcher.rs (TODO: implement once diagnostics ready)
|
||||
- ty_test compiles successfully with warnings (unused code, expected at this stage)
|
||||
|
|
@ -245,10 +245,13 @@ 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),
|
||||
}
|
||||
|
||||
impl<'a> UnparsedAssertion<'a> {
|
||||
/// Returns `Some(_)` if the comment starts with `# error:` or `# revealed:`,
|
||||
/// 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();
|
||||
|
|
@ -259,6 +262,7 @@ impl<'a> UnparsedAssertion<'a> {
|
|||
match keyword {
|
||||
"revealed" => Some(Self::Revealed(body)),
|
||||
"error" => Some(Self::Error(body)),
|
||||
"hover" | "↓ hover" => Some(Self::Hover(body)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -276,6 +280,9 @@ 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -285,6 +292,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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -297,6 +305,9 @@ pub(crate) enum ParsedAssertion<'a> {
|
|||
|
||||
/// An `# error:` assertion.
|
||||
Error(ErrorAssertion<'a>),
|
||||
|
||||
/// A `# hover:` assertion.
|
||||
Hover(HoverAssertion<'a>),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ParsedAssertion<'_> {
|
||||
|
|
@ -304,6 +315,7 @@ impl std::fmt::Display for ParsedAssertion<'_> {
|
|||
match self {
|
||||
Self::Revealed(expected_type) => write!(f, "revealed: {expected_type}"),
|
||||
Self::Error(assertion) => assertion.fmt(f),
|
||||
Self::Hover(assertion) => assertion.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -343,6 +355,38 @@ impl std::fmt::Display for ErrorAssertion<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A parsed and validated `# hover:` assertion comment.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct HoverAssertion<'a> {
|
||||
/// The column where the down arrow appears in the assertion comment.
|
||||
/// This indicates the position in the next line where we should hover.
|
||||
pub(crate) column: OneIndexed,
|
||||
|
||||
/// The expected type at the hover position.
|
||||
pub(crate) expected_type: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> HoverAssertion<'a> {
|
||||
fn from_str(source: &'a str) -> Result<Self, HoverAssertionParseError> {
|
||||
if source.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
|
||||
Ok(Self {
|
||||
column: OneIndexed::from_zero_indexed(0), // Placeholder, will be set by matcher
|
||||
expected_type: source,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HoverAssertion<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "hover: {}", self.expected_type)
|
||||
}
|
||||
}
|
||||
|
||||
/// A parser to convert a string into a [`ErrorAssertion`].
|
||||
#[derive(Debug, Clone)]
|
||||
struct ErrorAssertionParser<'a> {
|
||||
|
|
@ -454,13 +498,15 @@ impl<'a> ErrorAssertionParser<'a> {
|
|||
|
||||
/// Enumeration of ways in which parsing an assertion comment can fail.
|
||||
///
|
||||
/// The assertion comment could be either a "revealed" assertion or an "error" assertion.
|
||||
/// The assertion comment could be a "revealed", "error", or "hover" assertion.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum PragmaParseError<'a> {
|
||||
#[error("Must specify which type should be revealed")]
|
||||
EmptyRevealTypeAssertion,
|
||||
#[error("{0}")]
|
||||
ErrorAssertionParseError(ErrorAssertionParseError<'a>),
|
||||
#[error("{0}")]
|
||||
HoverAssertionParseError(HoverAssertionParseError),
|
||||
}
|
||||
|
||||
/// Enumeration of ways in which parsing an *error* assertion comment can fail.
|
||||
|
|
@ -486,6 +532,15 @@ pub(crate) enum ErrorAssertionParseError<'a> {
|
|||
UnexpectedCharacter { character: char, offset: usize },
|
||||
}
|
||||
|
||||
/// Enumeration of ways in which parsing a *hover* assertion comment can fail.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum HoverAssertionParseError {
|
||||
#[error("Hover assertion must contain a down arrow (↓) to indicate position")]
|
||||
MissingDownArrow,
|
||||
#[error("Must specify which type to expect at hover position")]
|
||||
EmptyType,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -372,6 +372,10 @@ impl Matcher {
|
|||
});
|
||||
matched_revealed_type.is_some()
|
||||
}
|
||||
ParsedAssertion::Hover(_hover) => {
|
||||
// TODO: Implement hover matching once hover diagnostic infrastructure is in place
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue