1use crate::crate_name::CrateName;
7use crate::error::ParseError;
8use crate::file_path::WorkspaceFilePath;
9use crate::path::SymbolPath;
10use crate::resolver::{CrateLayout, EntryPoint};
11
12#[derive(Debug, Clone)]
38pub struct SymbolPathResolver {
39 crate_name: CrateName,
40 layout: CrateLayout,
41 entry_point: EntryPoint,
42}
43
44impl SymbolPathResolver {
45 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 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 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 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 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 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 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 pub fn crate_name(&self) -> &CrateName {
143 &self.crate_name
144 }
145
146 pub fn entry_point(&self) -> EntryPoint {
148 self.entry_point
149 }
150
151 pub fn layout(&self) -> &CrateLayout {
153 &self.layout
154 }
155
156 pub fn resolve_module(&self, path: &WorkspaceFilePath) -> Result<SymbolPath, ParseError> {
160 SymbolPath::from_file_path(&self.crate_name, path)
161 }
162
163 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 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 pub fn module_path_str(&self, path: &WorkspaceFilePath) -> String {
194 SymbolPath::module_path_str(path)
195 }
196
197 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 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 if depth == 0 {
250 return format!("{}/{}", src_prefix, entry_file);
251 }
252
253 match depth {
259 0..=2 => format!("{}/{}", src_prefix, entry_file),
260 _ => {
261 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 #[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 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 let item_symbol = symbol.child("MyStruct").unwrap();
415 assert_eq!(item_symbol.to_string(), "my_crate::foo::bar::MyStruct");
416
417 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 #[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 let resolver =
484 SymbolPathResolver::with_layout("my_crate", CrateLayout::in_crates("my-crate"));
485 let workspace_root = Arc::from(Path::new("/project"));
486
487 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}