Skip to main content

ryo_symbol/
symbol_resolver.rs

1//! SymbolPathResolver - WorkspaceFilePath → SymbolPath conversion
2//!
3//! Centralizes the logic for converting file paths to symbol paths.
4//! This is the reverse of FilePathResolver.
5
6use crate::crate_name::CrateName;
7use crate::error::ParseError;
8use crate::file_path::WorkspaceFilePath;
9use crate::path::SymbolPath;
10use crate::resolver::{CrateLayout, EntryPoint};
11
12/// Resolves WorkspaceFilePath to SymbolPath
13///
14/// # Design
15///
16/// This resolver centralizes the conversion from file paths to Rust symbol paths.
17/// It wraps the crate name and layout, providing methods for bidirectional conversion
18/// between file paths and symbol paths.
19///
20/// # Example
21///
22/// ```ignore
23/// // For a crate in crates/ directory
24/// let resolver = SymbolPathResolver::with_layout(
25///     "my_crate",
26///     CrateLayout::in_crates("my-crate"),
27/// );
28///
29/// // Module path from file
30/// let module = resolver.resolve_module(&file_path)?;
31/// // → my_crate::foo::bar
32///
33/// // Reverse: symbol to file
34/// let file = resolver.to_workspace_file_path(&symbol, &workspace_root);
35/// // → crates/my-crate/src/foo/bar.rs
36/// ```
37#[derive(Debug, Clone)]
38pub struct SymbolPathResolver {
39    crate_name: CrateName,
40    layout: CrateLayout,
41    entry_point: EntryPoint,
42}
43
44impl SymbolPathResolver {
45    /// Create a resolver for a crate (assumes Root layout and Lib entry point)
46    ///
47    /// For workspace crates in `crates/` directory, use `with_layout` instead.
48    pub fn new(crate_name: impl AsRef<str>) -> Self {
49        Self {
50            crate_name: CrateName::new_unchecked(crate_name.as_ref()),
51            layout: CrateLayout::Root,
52            entry_point: EntryPoint::Lib,
53        }
54    }
55
56    /// Create a resolver with explicit layout (assumes Lib entry point)
57    ///
58    /// # Example
59    ///
60    /// ```ignore
61    /// // For crates/my-crate/src/*.rs
62    /// let resolver = SymbolPathResolver::with_layout(
63    ///     "my_crate",
64    ///     CrateLayout::in_crates("my-crate"),
65    /// );
66    /// ```
67    pub fn with_layout(crate_name: impl AsRef<str>, layout: CrateLayout) -> Self {
68        Self {
69            crate_name: CrateName::new_unchecked(crate_name.as_ref()),
70            layout,
71            entry_point: EntryPoint::Lib,
72        }
73    }
74
75    /// Create a resolver with explicit layout and entry point
76    ///
77    /// # Example
78    ///
79    /// ```ignore
80    /// // For a binary crate: crates/my-crate/src/main.rs
81    /// let resolver = SymbolPathResolver::with_layout_and_entry(
82    ///     "my_crate",
83    ///     CrateLayout::in_crates("my-crate"),
84    ///     EntryPoint::Bin,
85    /// );
86    /// ```
87    pub fn with_layout_and_entry(
88        crate_name: impl AsRef<str>,
89        layout: CrateLayout,
90        entry_point: EntryPoint,
91    ) -> Self {
92        Self {
93            crate_name: CrateName::new_unchecked(crate_name.as_ref()),
94            layout,
95            entry_point,
96        }
97    }
98
99    /// Create a resolver from a CrateName (assumes Root layout and Lib entry point)
100    pub fn from_crate_name(crate_name: CrateName) -> Self {
101        Self {
102            crate_name,
103            layout: CrateLayout::Root,
104            entry_point: EntryPoint::Lib,
105        }
106    }
107
108    /// Create a resolver from a CrateName with explicit layout (assumes Lib entry point)
109    pub fn from_crate_name_with_layout(crate_name: CrateName, layout: CrateLayout) -> Self {
110        Self {
111            crate_name,
112            layout,
113            entry_point: EntryPoint::Lib,
114        }
115    }
116
117    /// Create a resolver from a CrateName with explicit layout and entry point
118    pub fn from_crate_name_with_layout_and_entry(
119        crate_name: CrateName,
120        layout: CrateLayout,
121        entry_point: EntryPoint,
122    ) -> Self {
123        Self {
124            crate_name,
125            layout,
126            entry_point,
127        }
128    }
129
130    /// Create a resolver from a WorkspaceFilePath
131    ///
132    /// Extracts the crate name and infers both layout and entry point from the file path.
133    pub fn from_workspace_path(path: &WorkspaceFilePath) -> Result<Self, ParseError> {
134        Ok(Self {
135            crate_name: path.crate_name().clone(),
136            layout: CrateLayout::from_workspace_file_path(path),
137            entry_point: EntryPoint::from_path(path.as_relative()),
138        })
139    }
140
141    /// Get the crate name
142    pub fn crate_name(&self) -> &CrateName {
143        &self.crate_name
144    }
145
146    /// Get the entry point
147    pub fn entry_point(&self) -> EntryPoint {
148        self.entry_point
149    }
150
151    /// Get the crate layout
152    pub fn layout(&self) -> &CrateLayout {
153        &self.layout
154    }
155
156    /// Resolve file path to module path (SymbolPath)
157    ///
158    /// e.g., "src/foo/bar.rs" → "my_crate::foo::bar"
159    pub fn resolve_module(&self, path: &WorkspaceFilePath) -> Result<SymbolPath, ParseError> {
160        SymbolPath::from_file_path(&self.crate_name, path)
161    }
162
163    /// Resolve item in file
164    ///
165    /// e.g., "src/lib.rs" + "MyStruct" → "my_crate::MyStruct"
166    pub fn resolve_item(
167        &self,
168        path: &WorkspaceFilePath,
169        item_name: &str,
170    ) -> Result<SymbolPath, ParseError> {
171        let module = self.resolve_module(path)?;
172        module.child(item_name)
173    }
174
175    /// Resolve nested item in file
176    ///
177    /// e.g., "src/lib.rs" + ["Foo", "new"] → "my_crate::Foo::new"
178    pub fn resolve_nested(
179        &self,
180        path: &WorkspaceFilePath,
181        segments: &[&str],
182    ) -> Result<SymbolPath, ParseError> {
183        let mut current = self.resolve_module(path)?;
184        for seg in segments {
185            current = current.child(*seg)?;
186        }
187        Ok(current)
188    }
189
190    /// Get module path string (without parsing to SymbolPath)
191    ///
192    /// Useful when you need the string representation directly.
193    pub fn module_path_str(&self, path: &WorkspaceFilePath) -> String {
194        SymbolPath::module_path_str(path)
195    }
196
197    // ========================================================================
198    // Reverse conversion: SymbolPath → WorkspaceFilePath
199    // ========================================================================
200
201    /// Convert SymbolPath to WorkspaceFilePath (reverse of resolve_module)
202    ///
203    /// Determines the file path where a symbol should be defined.
204    ///
205    /// # Rules
206    ///
207    /// - `crate` → `src/lib.rs`
208    /// - `crate::Item` → `src/lib.rs` (item in crate root)
209    /// - `crate::foo::Item` → `src/foo.rs` (item in module foo)
210    /// - `crate::foo::bar::Item` → `src/foo/bar.rs` (item in nested module)
211    ///
212    /// # Arguments
213    ///
214    /// - `path`: The symbol path to convert
215    /// - `workspace_root`: The workspace root for creating WorkspaceFilePath
216    ///
217    /// # Example
218    ///
219    /// ```ignore
220    /// let resolver = SymbolPathResolver::new("my_crate");
221    /// let symbol = SymbolPath::parse("my_crate::models::User")?;
222    /// let file = resolver.to_workspace_file_path(&symbol, &workspace_root);
223    /// // → src/models.rs
224    /// ```
225    pub fn to_workspace_file_path(
226        &self,
227        symbol_path: &SymbolPath,
228        workspace_root: std::sync::Arc<std::path::Path>,
229    ) -> WorkspaceFilePath {
230        let relative = self.symbol_path_to_relative(symbol_path);
231        WorkspaceFilePath::new_unchecked(
232            std::path::PathBuf::from(relative),
233            workspace_root,
234            self.crate_name.clone(),
235        )
236    }
237
238    /// Convert SymbolPath to relative file path string
239    ///
240    /// This is the core logic for reverse conversion.
241    /// Uses the crate layout and entry point to determine the correct path.
242    pub fn symbol_path_to_relative(&self, symbol_path: &SymbolPath) -> String {
243        let depth = symbol_path.depth();
244        let src_dir = self.layout.src_dir();
245        let src_prefix = src_dir.to_string_lossy();
246        let entry_file = self.entry_point.file_name();
247
248        // Handle edge cases
249        if depth == 0 {
250            return format!("{}/{}", src_prefix, entry_file);
251        }
252
253        // depth 1: crate → {src}/{entry} (lib.rs or main.rs)
254        // depth 2: crate::Item → {src}/{entry}
255        // depth 3: crate::mod::Item → {src}/mod.rs
256        // depth 4: crate::mod1::mod2::Item → {src}/mod1/mod2.rs
257
258        match depth {
259            0..=2 => format!("{}/{}", src_prefix, entry_file),
260            _ => {
261                // Module segments are from index 1 to (depth - 2) inclusive
262                // For depth=3: segments[1] is the module
263                // For depth=4: segments[1], segments[2] are the modules (but last one is in parent)
264                //
265                // Actually, let's think about this more carefully:
266                // crate::foo::Bar (depth=3) → item Bar is in module foo → {src}/foo.rs
267                // crate::foo::bar::Baz (depth=4) → item Baz is in module foo::bar → {src}/foo/bar.rs
268                //
269                // So we need segments from 1 to (depth - 2) for the file path
270
271                let mut parts = Vec::new();
272                for i in 1..=(depth - 2) {
273                    if let Some(seg) = symbol_path.segment(i) {
274                        parts.push(seg.name().to_string());
275                    }
276                }
277
278                if parts.is_empty() {
279                    format!("{}/{}", src_prefix, entry_file)
280                } else {
281                    format!("{}/{}.rs", src_prefix, parts.join("/"))
282                }
283            }
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    fn make_path(relative: &str) -> WorkspaceFilePath {
293        WorkspaceFilePath::new_for_test(relative, "/workspace", "my_crate")
294    }
295
296    #[test]
297    fn test_new() {
298        let resolver = SymbolPathResolver::new("my_crate");
299        assert_eq!(resolver.crate_name().as_str(), "my_crate");
300    }
301
302    #[test]
303    fn test_from_workspace_path() {
304        let path = make_path("src/lib.rs");
305        let resolver = SymbolPathResolver::from_workspace_path(&path).unwrap();
306        assert_eq!(resolver.crate_name().as_str(), "my_crate");
307    }
308
309    #[test]
310    fn test_resolve_module_lib() {
311        let resolver = SymbolPathResolver::new("my_crate");
312        let path = make_path("src/lib.rs");
313        let symbol = resolver.resolve_module(&path).unwrap();
314        assert_eq!(symbol.to_string(), "my_crate");
315    }
316
317    #[test]
318    fn test_resolve_module_nested() {
319        let resolver = SymbolPathResolver::new("my_crate");
320        let path = make_path("src/foo/bar.rs");
321        let symbol = resolver.resolve_module(&path).unwrap();
322        assert_eq!(symbol.to_string(), "my_crate::foo::bar");
323    }
324
325    #[test]
326    fn test_resolve_item() {
327        let resolver = SymbolPathResolver::new("my_crate");
328        let path = make_path("src/lib.rs");
329        let symbol = resolver.resolve_item(&path, "MyStruct").unwrap();
330        assert_eq!(symbol.to_string(), "my_crate::MyStruct");
331    }
332
333    #[test]
334    fn test_resolve_nested() {
335        let resolver = SymbolPathResolver::new("my_crate");
336        let path = make_path("src/lib.rs");
337        let symbol = resolver.resolve_nested(&path, &["Foo", "new"]).unwrap();
338        assert_eq!(symbol.to_string(), "my_crate::Foo::new");
339    }
340
341    #[test]
342    fn test_module_path_str() {
343        let resolver = SymbolPathResolver::new("my_crate");
344        let path = make_path("src/foo/bar.rs");
345        assert_eq!(resolver.module_path_str(&path), "my_crate::foo::bar");
346    }
347
348    // ========================================================================
349    // Reverse conversion tests
350    // ========================================================================
351
352    #[test]
353    fn test_symbol_path_to_relative_crate_root() {
354        let resolver = SymbolPathResolver::new("test_crate");
355        let symbol = SymbolPath::parse("test_crate").unwrap();
356        assert_eq!(resolver.symbol_path_to_relative(&symbol), "src/lib.rs");
357    }
358
359    #[test]
360    fn test_symbol_path_to_relative_crate_item() {
361        let resolver = SymbolPathResolver::new("test_crate");
362        let symbol = SymbolPath::parse("test_crate::Config").unwrap();
363        assert_eq!(resolver.symbol_path_to_relative(&symbol), "src/lib.rs");
364    }
365
366    #[test]
367    fn test_symbol_path_to_relative_module_item() {
368        let resolver = SymbolPathResolver::new("test_crate");
369        let symbol = SymbolPath::parse("test_crate::models::User").unwrap();
370        assert_eq!(resolver.symbol_path_to_relative(&symbol), "src/models.rs");
371    }
372
373    #[test]
374    fn test_symbol_path_to_relative_nested_module() {
375        let resolver = SymbolPathResolver::new("test_crate");
376        let symbol = SymbolPath::parse("test_crate::foo::bar::Baz").unwrap();
377        assert_eq!(resolver.symbol_path_to_relative(&symbol), "src/foo/bar.rs");
378    }
379
380    #[test]
381    fn test_symbol_path_to_relative_deep_nested() {
382        let resolver = SymbolPathResolver::new("test_crate");
383        let symbol = SymbolPath::parse("test_crate::a::b::c::Item").unwrap();
384        assert_eq!(resolver.symbol_path_to_relative(&symbol), "src/a/b/c.rs");
385    }
386
387    #[test]
388    fn test_to_workspace_file_path() {
389        use std::path::Path;
390        use std::sync::Arc;
391
392        let resolver = SymbolPathResolver::new("test_crate");
393        let symbol = SymbolPath::parse("test_crate::models::User").unwrap();
394        let workspace_root = Arc::from(Path::new("/project"));
395
396        let file_path = resolver.to_workspace_file_path(&symbol, workspace_root);
397        assert_eq!(file_path.as_relative().to_str().unwrap(), "src/models.rs");
398    }
399
400    #[test]
401    fn test_roundtrip_conversion() {
402        use std::path::Path;
403        use std::sync::Arc;
404
405        let resolver = SymbolPathResolver::new("my_crate");
406        let workspace_root = Arc::from(Path::new("/project"));
407
408        // Forward: file → symbol
409        let original_file = make_path("src/foo/bar.rs");
410        let symbol = resolver.resolve_module(&original_file).unwrap();
411        assert_eq!(symbol.to_string(), "my_crate::foo::bar");
412
413        // Add an item
414        let item_symbol = symbol.child("MyStruct").unwrap();
415        assert_eq!(item_symbol.to_string(), "my_crate::foo::bar::MyStruct");
416
417        // Reverse: symbol → file
418        let recovered_file = resolver.to_workspace_file_path(&item_symbol, workspace_root);
419        assert_eq!(
420            recovered_file.as_relative().to_str().unwrap(),
421            "src/foo/bar.rs"
422        );
423    }
424
425    // ========================================================================
426    // CrateLayout tests
427    // ========================================================================
428
429    #[test]
430    fn test_with_layout_in_crates() {
431        let resolver =
432            SymbolPathResolver::with_layout("my_crate", CrateLayout::in_crates("my-crate"));
433        let symbol = SymbolPath::parse("my_crate::Config").unwrap();
434        assert_eq!(
435            resolver.symbol_path_to_relative(&symbol),
436            "crates/my-crate/src/lib.rs"
437        );
438    }
439
440    #[test]
441    fn test_with_layout_in_crates_nested() {
442        let resolver =
443            SymbolPathResolver::with_layout("my_crate", CrateLayout::in_crates("my-crate"));
444        let symbol = SymbolPath::parse("my_crate::models::User").unwrap();
445        assert_eq!(
446            resolver.symbol_path_to_relative(&symbol),
447            "crates/my-crate/src/models.rs"
448        );
449    }
450
451    #[test]
452    fn test_with_layout_custom() {
453        let resolver =
454            SymbolPathResolver::with_layout("my_crate", CrateLayout::custom("packages/core"));
455        let symbol = SymbolPath::parse("my_crate::models::User").unwrap();
456        assert_eq!(
457            resolver.symbol_path_to_relative(&symbol),
458            "packages/core/src/models.rs"
459        );
460    }
461
462    #[test]
463    fn test_from_workspace_path_infers_layout() {
464        let path =
465            WorkspaceFilePath::new_for_test("crates/my-crate/src/lib.rs", "/workspace", "my_crate");
466        let resolver = SymbolPathResolver::from_workspace_path(&path).unwrap();
467
468        assert_eq!(resolver.crate_name().as_str(), "my_crate");
469        assert_eq!(
470            resolver.layout(),
471            &CrateLayout::InCrates {
472                crate_dir_name: "my-crate".to_string()
473            }
474        );
475    }
476
477    #[test]
478    fn test_roundtrip_with_workspace_layout() {
479        use std::path::Path;
480        use std::sync::Arc;
481
482        // Create resolver with workspace layout
483        let resolver =
484            SymbolPathResolver::with_layout("my_crate", CrateLayout::in_crates("my-crate"));
485        let workspace_root = Arc::from(Path::new("/project"));
486
487        // Symbol → File
488        let symbol = SymbolPath::parse("my_crate::foo::bar::Item").unwrap();
489        let file = resolver.to_workspace_file_path(&symbol, workspace_root);
490
491        assert_eq!(
492            file.as_relative().to_str().unwrap(),
493            "crates/my-crate/src/foo/bar.rs"
494        );
495    }
496}