Skip to main content

ryo_mutations/analyzer/
mod.rs

1//! rust-analyzer Integration: LSP-based code intelligence for mutations
2//!
3//! This module provides integration with [rust-analyzer](https://rust-analyzer.github.io/)
4//! via the Language Server Protocol (LSP). It enables semantic-aware code transformations
5//! that require type information beyond what pure AST analysis can provide.
6//!
7//! # Architecture
8//!
9//! ```text
10//! ┌─────────────────────────────────────────────────────────────────┐
11//! │                    rust-analyzer Integration                     │
12//! ├─────────────────────────────────────────────────────────────────┤
13//! │                                                                  │
14//! │  ┌──────────────┐    LSP Protocol    ┌──────────────────────┐  │
15//! │  │ ryo-mutations│◄──────────────────►│ rust-analyzer        │  │
16//! │  │              │                     │                      │  │
17//! │  │ - Mutations  │  textDocument/      │ - Type inference     │  │
18//! │  │ - PureFile   │  inlayHint          │ - Semantic tokens    │  │
19//! │  │              │  textDocument/      │ - Code actions       │  │
20//! │  │              │  codeAction         │ - Diagnostics        │  │
21//! │  └──────────────┘                     └──────────────────────┘  │
22//! │         │                                       │                │
23//! │         ▼                                       ▼                │
24//! │  ┌──────────────────────────────────────────────────────────┐  │
25//! │  │                    Enriched Mutations                     │  │
26//! │  │                                                           │  │
27//! │  │  - CloneOnCopy with accurate Copy detection               │  │
28//! │  │  - (v0.1.0) AddExplicitType / FillMatchArms /             │  │
29//! │  │    AddMissingFields are NOT YET shipped — type-inference  │  │
30//! │  │    infrastructure required                                │  │
31//! │  └──────────────────────────────────────────────────────────┘  │
32//! └─────────────────────────────────────────────────────────────────┘
33//! ```
34//!
35//! # LSP Endpoints Used
36//!
37//! ## InlayHints (`textDocument/inlayHint`)
38//!
39//! Provides type information that the compiler infers but isn't written in code:
40//!
41//! | Hint Kind | Example | Use Case |
42//! |-----------|---------|----------|
43//! | Type | `let x: i32 = ...` | Add explicit type annotations |
44//! | Parameter | `foo(/* name: */ value)` | Parameter name completion |
45//! | Chaining | `.map(...): Vec<_>` | Method chain type tracking |
46//!
47//! ## CodeAction (`textDocument/codeAction`)
48//!
49//! rust-analyzer "assists" that can be converted to Mutations:
50//!
51//! | Assist | Mutation | Description |
52//! |--------|----------|-------------|
53//! | `fill_match_arms` | _(deferred — needs type inference)_ | Add missing match arms |
54//! | `add_missing_fields` | _(deferred — needs type inference)_ | Add missing struct fields |
55//! | `inline_type_alias` | `InlineTypeAliasMutation` | Expand type alias |
56//! | `extract_type_alias` | `ExtractTypeAliasMutation` | Create type alias |
57//! | `add_explicit_type` | _(deferred — needs type inference)_ | Add type annotation |
58//! | `replace_if_let_with_match` | `IfLetToMatchMutation` | Convert if let to match |
59//! | `replace_match_with_if_let` | `MatchToIfLetMutation` | Convert match to if let |
60//!
61//! `fill_match_arms` / `add_missing_fields` / `add_explicit_type` derivations
62//! were removed in v0.1.0 because their AST-level apply paths required
63//! type-inference infrastructure that has not yet landed; they will return
64//! when that infrastructure is in place.
65//!
66//! ## Hover (`textDocument/hover`)
67//!
68//! Type information and documentation for symbols.
69//!
70//! ## SemanticTokens (`textDocument/semanticTokens`)
71//!
72//! Token classification for semantic highlighting:
73//! - Variable modifiers: `mutable`, `consuming`, `reference`
74//! - Type modifiers: `copy`, `associated`
75//!
76//! # Usage
77//!
78//! ```rust,ignore
79//! use ryo_mutations::analyzer::{AnalyzerClient, InlayHintQuery};
80//!
81//! // Connect to rust-analyzer
82//! let client = AnalyzerClient::new("/path/to/project")?;
83//!
84//! // Get type hints for a file
85//! let hints = client.inlay_hints("src/lib.rs")?;
86//!
87//! // Use hints to enrich mutations
88//! for hint in hints.type_hints() {
89//!     if hint.is_copy_type() {
90//!         // Can safely apply CloneOnCopy mutation
91//!     }
92//! }
93//!
94//! // Get available code actions at a position
95//! let actions = client.code_actions("src/lib.rs", line, column)?;
96//!
97//! // Convert to mutations
98//! for action in actions {
99//!     if let Some(mutation) = action.to_mutation() {
100//!         println!("Available: {}", mutation.describe());
101//!     }
102//! }
103//! ```
104//!
105//! # Relationship with Clippy Integration
106//!
107//! | Feature | Clippy | rust-analyzer |
108//! |---------|--------|---------------|
109//! | Type info | No | Yes (InlayHints) |
110//! | Lint fixes | Yes (suggestions) | Yes (CodeActions) |
111//! | Refactoring | Limited | Extensive (assists) |
112//! | Batch apply | Yes | Per-file |
113//!
114//! For best results, combine both:
115//! - Use Clippy for lint-driven transformations
116//! - Use rust-analyzer for semantic-aware refactoring
117//!
118//! # Performance Considerations
119//!
120//! - rust-analyzer startup can take several seconds for large projects
121//! - InlayHints are computed lazily, request only needed ranges
122//! - Consider caching results for repeated operations
123//!
124//! # Future Extensions
125//!
126//! - [ ] Workspace-wide rename with type checking
127//! - [ ] Automatic import resolution
128//! - [ ] Trait implementation scaffolding
129//! - [ ] Lifetime inference and annotation
130
131mod code_action;
132mod inlay_hints;
133
134pub use code_action::{AnalyzerCodeAction, CodeActionKind, CodeActionToMutation};
135pub use inlay_hints::{InlayHint, InlayHintKind, InlayHintQuery, TypeHint};
136
137use std::path::Path;
138
139/// Known rust-analyzer assist IDs that map to mutations
140pub mod assists {
141    pub const FILL_MATCH_ARMS: &str = "fill_match_arms";
142    pub const ADD_MISSING_FIELDS: &str = "add_missing_impl_members";
143    pub const ADD_EXPLICIT_TYPE: &str = "add_explicit_type";
144    pub const INLINE_TYPE_ALIAS: &str = "inline_type_alias";
145    pub const EXTRACT_TYPE_ALIAS: &str = "extract_type_alias";
146    pub const REPLACE_IF_LET_WITH_MATCH: &str = "replace_if_let_with_match";
147    pub const REPLACE_MATCH_WITH_IF_LET: &str = "replace_match_with_if_let";
148    pub const CONVERT_TO_GUARDED_RETURN: &str = "convert_to_guarded_return";
149    pub const INLINE_FUNCTION: &str = "inline_into_callers";
150    pub const EXTRACT_FUNCTION: &str = "extract_function";
151}
152
153/// Common Copy types for heuristic detection
154pub mod copy_types {
155    /// Primitive types that are always Copy
156    pub const PRIMITIVES: &[&str] = &[
157        "bool", "char", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64",
158        "u128", "usize", "f32", "f64",
159    ];
160
161    /// Common std types that are Copy
162    pub const STD_COPY: &[&str] = &[
163        "NonZeroI8",
164        "NonZeroI16",
165        "NonZeroI32",
166        "NonZeroI64",
167        "NonZeroI128",
168        "NonZeroIsize",
169        "NonZeroU8",
170        "NonZeroU16",
171        "NonZeroU32",
172        "NonZeroU64",
173        "NonZeroU128",
174        "NonZeroUsize",
175        "Duration",
176        "Instant",
177        "IpAddr",
178        "Ipv4Addr",
179        "Ipv6Addr",
180        "SocketAddr",
181        "SocketAddrV4",
182        "SocketAddrV6",
183        "Ordering",
184        "TypeId",
185    ];
186
187    /// Check if a type name is a known Copy type
188    pub fn is_known_copy(type_name: &str) -> bool {
189        // Strip references and generics for basic check
190        let base = type_name
191            .trim_start_matches('&')
192            .trim_start_matches("mut ")
193            .split('<')
194            .next()
195            .unwrap_or(type_name)
196            .trim();
197
198        PRIMITIVES.contains(&base) || STD_COPY.contains(&base)
199    }
200}
201
202/// Client for communicating with rust-analyzer
203///
204/// This is a lightweight wrapper that can use an existing LSP connection
205/// or spawn a new rust-analyzer instance.
206pub struct AnalyzerClient {
207    workspace_root: std::path::PathBuf,
208    // In the future, this will hold the LSP connection
209    // For now, we provide utilities that work with LSP responses
210}
211
212impl AnalyzerClient {
213    /// Create a new analyzer client for a workspace
214    pub fn new(workspace_root: impl AsRef<Path>) -> Self {
215        Self {
216            workspace_root: workspace_root.as_ref().to_path_buf(),
217        }
218    }
219
220    /// Get the workspace root
221    pub fn workspace_root(&self) -> &Path {
222        &self.workspace_root
223    }
224
225    /// Check if a type is Copy based on known patterns
226    ///
227    /// This is a heuristic check. For accurate results, use InlayHints
228    /// with actual type information from rust-analyzer.
229    pub fn is_likely_copy(&self, type_name: &str) -> bool {
230        copy_types::is_known_copy(type_name)
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_is_known_copy_primitives() {
240        assert!(copy_types::is_known_copy("i32"));
241        assert!(copy_types::is_known_copy("bool"));
242        assert!(copy_types::is_known_copy("f64"));
243        assert!(copy_types::is_known_copy("usize"));
244    }
245
246    #[test]
247    fn test_is_known_copy_with_reference() {
248        assert!(copy_types::is_known_copy("&i32"));
249        assert!(copy_types::is_known_copy("&mut bool"));
250    }
251
252    #[test]
253    fn test_is_known_copy_std_types() {
254        assert!(copy_types::is_known_copy("Duration"));
255        assert!(copy_types::is_known_copy("Ordering"));
256    }
257
258    #[test]
259    fn test_is_not_copy() {
260        assert!(!copy_types::is_known_copy("String"));
261        assert!(!copy_types::is_known_copy("Vec<i32>"));
262        assert!(!copy_types::is_known_copy("Box<dyn Trait>"));
263    }
264}