Skip to main content

ryo_symbol/
path.rs

1//! SymbolPath - Rust ecosystem-wide unique symbol identifier
2
3use std::fmt::{self, Display, Formatter};
4use std::str::FromStr;
5
6use compact_str::CompactString;
7use serde::{Deserialize, Serialize};
8use smallvec::SmallVec;
9
10use crate::crate_name::CrateName;
11use crate::error::ParseError;
12use crate::file_path::WorkspaceFilePath;
13use crate::var_scope::VarScope;
14
15/// Path segment
16///
17/// A single component of a SymbolPath. Just a name string.
18/// Kind information is managed by SymbolRegistry, not by Segment.
19///
20/// # Invariants
21/// - Must be a valid Rust identifier, OR
22/// - Must be a numeric string (for tuple fields like `0`, `1`)
23/// - Must be a scope marker (for InSymbol: `$param`, `$var`, `$field`)
24#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub struct Segment(CompactString);
26
27impl Segment {
28    /// Create a new segment with validation
29    ///
30    /// # Errors
31    /// - `ParseError::InvalidIdentifier`: Not a valid Rust identifier
32    pub fn new(name: impl AsRef<str>) -> Result<Self, ParseError> {
33        let name = name.as_ref();
34        validate_rust_identifier(name)?;
35        Ok(Self(name.into()))
36    }
37
38    /// Create without validation (internal use)
39    pub(crate) fn new_unchecked(name: impl AsRef<str>) -> Self {
40        Self(name.as_ref().into())
41    }
42
43    /// Get the segment name
44    pub fn name(&self) -> &str {
45        &self.0
46    }
47
48    /// Create a tuple field segment (numeric)
49    ///
50    /// # Examples
51    /// ```
52    /// # use ryo_symbol::Segment;
53    /// let seg = Segment::tuple_field(0);
54    /// assert_eq!(seg.name(), "0");
55    /// ```
56    pub fn tuple_field(index: usize) -> Self {
57        Self(index.to_string().into())
58    }
59
60    /// Check if this is a tuple field (all digits)
61    pub fn is_tuple_field(&self) -> bool {
62        !self.0.is_empty() && self.0.chars().all(|c| c.is_ascii_digit())
63    }
64
65    /// Get tuple field index if this is a tuple field
66    pub fn tuple_field_index(&self) -> Option<usize> {
67        if self.is_tuple_field() {
68            self.0.parse().ok()
69        } else {
70            None
71        }
72    }
73
74    /// Check if this is a scope marker ($param, $var, $field, $body)
75    pub fn is_scope_marker(&self) -> bool {
76        self.0.starts_with('$')
77    }
78
79    /// Get the VarScope if this is a scope marker
80    pub fn as_var_scope(&self) -> Option<VarScope> {
81        VarScope::from_segment(&self.0)
82    }
83
84    // ========== Scope Segment Constructors ==========
85
86    /// Parameter scope segment (`$param`)
87    pub fn param_scope() -> Self {
88        Self::new_unchecked("$param")
89    }
90
91    /// Variable scope segment (`$var`)
92    pub fn local_scope() -> Self {
93        Self::new_unchecked("$var")
94    }
95
96    /// Field scope segment (`$field`)
97    pub fn field_scope() -> Self {
98        Self::new_unchecked("$field")
99    }
100
101    /// Body scope segment (`$body`)
102    pub fn body_scope() -> Self {
103        Self::new_unchecked("$body")
104    }
105
106    /// Statement scope segment (`$stmt`)
107    pub fn stmt_scope() -> Self {
108        Self::new_unchecked("$stmt")
109    }
110
111    /// Expression scope segment (`$expr`)
112    pub fn expr_scope() -> Self {
113        Self::new_unchecked("$expr")
114    }
115
116    // ========== Impl Block Constructors ==========
117
118    /// Inherent impl segment: `<impl Type>`
119    pub fn inherent_impl(self_ty: &str) -> Self {
120        Self::new_unchecked(format!("<impl {}>", self_ty))
121    }
122
123    /// Trait impl segment: `<impl Trait for Type>`
124    pub fn trait_impl(trait_name: &str, self_ty: &str) -> Self {
125        Self::new_unchecked(format!("<impl {} for {}>", trait_name, self_ty))
126    }
127
128    // ========== Impl Block Checks ==========
129
130    /// Check if this is an impl block segment (`<impl ...>`)
131    pub fn is_impl(&self) -> bool {
132        self.0.starts_with("<impl ") && self.0.ends_with('>')
133    }
134
135    /// Check if this is a trait impl segment (`<impl Trait for Type>`)
136    pub fn is_trait_impl(&self) -> bool {
137        self.is_impl() && self.0.contains(" for ")
138    }
139
140    /// Extract the self type from an impl segment.
141    ///
142    /// - `<impl Type>` → `Some("Type")`
143    /// - `<impl Trait for Type>` → `Some("Type")`
144    /// - non-impl → `None`
145    pub fn impl_self_ty(&self) -> Option<&str> {
146        if !self.is_impl() {
147            return None;
148        }
149        // Strip "<impl " (6 chars) and ">" (1 char)
150        let inner = &self.0[6..self.0.len() - 1];
151        if let Some(pos) = inner.find(" for ") {
152            Some(&inner[pos + 5..])
153        } else {
154            Some(inner)
155        }
156    }
157
158    /// Extract the trait name from a trait impl segment.
159    ///
160    /// - `<impl Trait for Type>` → `Some("Trait")`
161    /// - `<impl Type>` → `None`
162    /// - non-impl → `None`
163    pub fn impl_trait(&self) -> Option<&str> {
164        if !self.is_impl() {
165            return None;
166        }
167        let inner = &self.0[6..self.0.len() - 1];
168        inner.find(" for ").map(|pos| &inner[..pos])
169    }
170
171    // ========== Scope Segment Checks ==========
172
173    /// Check if this is a parameter scope segment (`$param`)
174    pub fn is_param_scope(&self) -> bool {
175        self.0.as_str() == "$param"
176    }
177
178    /// Check if this is a local variable scope segment (`$var`)
179    pub fn is_local_scope(&self) -> bool {
180        self.0.as_str() == "$var"
181    }
182
183    /// Check if this is a field scope segment (`$field`)
184    pub fn is_field_scope(&self) -> bool {
185        self.0.as_str() == "$field"
186    }
187
188    /// Check if this is a body scope segment (`$body`)
189    pub fn is_body_scope(&self) -> bool {
190        self.0.as_str() == "$body"
191    }
192
193    /// Check if this is a statement scope segment (`$stmt`)
194    pub fn is_stmt_scope(&self) -> bool {
195        self.0.as_str() == "$stmt"
196    }
197
198    /// Check if this is an expression scope segment (`$expr`)
199    pub fn is_expr_scope(&self) -> bool {
200        self.0.as_str() == "$expr"
201    }
202}
203
204impl Display for Segment {
205    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
206        write!(f, "{}", self.0)
207    }
208}
209
210/// Validate a Rust identifier
211///
212/// Valid: `[a-zA-Z_][a-zA-Z0-9_]*` or all digits (tuple field) or scope marker ($...)
213fn validate_rust_identifier(s: &str) -> Result<(), ParseError> {
214    if s.is_empty() {
215        return Err(ParseError::InvalidIdentifier(s.to_string()));
216    }
217
218    // Tuple field (all digits) is OK
219    if s.chars().all(|c| c.is_ascii_digit()) {
220        return Ok(());
221    }
222
223    // Scope marker ($param, $var, $field, $body, $stmt, $expr) is OK
224    if s.starts_with('$') {
225        if VarScope::from_segment(s).is_some() || matches!(s, "$body" | "$stmt" | "$expr") {
226            return Ok(());
227        }
228        return Err(ParseError::InvalidIdentifier(s.to_string()));
229    }
230
231    // Impl block segment (e.g., "<impl Counter>", "<impl Trait for Type>")
232    if s.starts_with("<impl ") && s.ends_with('>') {
233        return Ok(());
234    }
235
236    // Rust identifier rules
237    let mut chars = s.chars();
238    let first = chars
239        .next()
240        .expect("non-empty checked at function entry (s.is_empty() guard)");
241    if !first.is_ascii_alphabetic() && first != '_' {
242        return Err(ParseError::InvalidIdentifier(s.to_string()));
243    }
244
245    for c in chars {
246        if !c.is_ascii_alphanumeric() && c != '_' {
247            return Err(ParseError::InvalidIdentifier(s.to_string()));
248        }
249    }
250
251    Ok(())
252}
253
254/// Rust ecosystem-wide unique symbol path
255///
256/// # Format
257/// ```text
258/// tokio::sync::Mutex::lock
259/// std::collections::HashMap::insert
260/// ryo_core::mutations::AddMethod::apply
261/// ```
262///
263/// # Invariants
264/// - segments is non-empty
265/// - First segment is always the crate name
266/// - Each segment is a valid Rust identifier (or tuple field, or scope marker)
267/// - crate_root_depth is either 1 (lib crate) or 2 (bin crate with main:: prefix)
268#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
269pub struct SymbolPath {
270    /// Path segments (crate name first)
271    ///
272    /// Up to 12 elements stored inline. Typical Rust code has 5-8 levels,
273    /// but nested impl blocks or macro-generated code can go deeper.
274    segments: SmallVec<[Segment; 12]>,
275
276    /// Depth to crate root (1 for lib, 2 for bin with main:: prefix)
277    ///
278    /// This is precomputed during construction for O(1) access.
279    /// - `my_lib::utils::Helper` → crate_root_depth = 1
280    /// - `main::my_app::utils::Helper` → crate_root_depth = 2
281    crate_root_depth: u8,
282}
283
284impl SymbolPath {
285    // ========== Public API (controlled creation) ==========
286
287    /// Create from file path (external API)
288    ///
289    /// This is the primary way to create a SymbolPath from outside the crate.
290    ///
291    /// # Arguments
292    /// - `crate_name`: The crate containing the file
293    /// - `path`: The file path within the workspace
294    ///
295    /// # Returns
296    /// A SymbolPath representing the module path for this file
297    pub fn from_file_path(
298        crate_name: &CrateName,
299        path: &WorkspaceFilePath,
300    ) -> Result<Self, ParseError> {
301        let mut builder = Self::builder(crate_name.as_str());
302
303        // Convert file path to module path
304        // e.g., "src/foo/bar.rs" -> ["foo", "bar"]
305        // e.g., "src/foo/mod.rs" -> ["foo"]
306        // e.g., "src/lib.rs" -> [] (crate root)
307        // e.g., "crates/my_crate/src/foo.rs" -> ["foo"]
308        let relative = path.as_relative();
309        let mut components: Vec<_> = relative
310            .components()
311            .filter_map(|c| c.as_os_str().to_str())
312            .collect();
313
314        // Handle "crates/<name>/src/" pattern - skip to src/
315        // This handles workspace paths like "crates/my_crate/src/lib.rs"
316        if let Some(src_idx) = components.iter().position(|&c| c == "src") {
317            components = components[src_idx..].to_vec();
318        }
319
320        // Remove "src" prefix if present
321        if components.first() == Some(&"src") {
322            components.remove(0);
323        }
324
325        // Process remaining components
326        for (i, component) in components.iter().enumerate() {
327            let is_last = i == components.len() - 1;
328
329            if is_last {
330                // Handle file name
331                let name = *component;
332                if name == "lib.rs" || name == "main.rs" || name == "mod.rs" {
333                    // These don't add to the path
334                    continue;
335                }
336                // Strip .rs extension
337                if let Some(module_name) = name.strip_suffix(".rs") {
338                    builder = builder.push(module_name);
339                }
340            } else {
341                // Directory name becomes module
342                builder = builder.push(*component);
343            }
344        }
345
346        builder.build()
347    }
348
349    /// Create from WorkspaceFilePath (uses embedded crate_name)
350    ///
351    /// This is the simplest way to create a SymbolPath from a file path.
352    /// Since WorkspaceFilePath now contains the crate_name, no external
353    /// provider is needed.
354    ///
355    /// # Example
356    /// ```ignore
357    /// let path = WorkspaceFilePath::new_for_test("src/foo/bar.rs", "/workspace", "my_crate");
358    /// let symbol = SymbolPath::from_workspace_file(&path)?;
359    /// // → my_crate::foo::bar
360    /// ```
361    pub fn from_workspace_file(path: &WorkspaceFilePath) -> Result<Self, ParseError> {
362        Self::from_file_path(path.crate_name(), path)
363    }
364
365    /// Create a symbol path for an item within a file
366    ///
367    /// # Arguments
368    /// - `path`: The file containing the item
369    /// - `item_name`: The name of the item (struct, fn, etc.)
370    ///
371    /// # Example
372    /// ```ignore
373    /// let path = WorkspaceFilePath::new_for_test("src/foo.rs", "/workspace", "my_crate");
374    /// let symbol = SymbolPath::item_in_file(&path, "MyStruct")?;
375    /// // → my_crate::foo::MyStruct
376    /// ```
377    pub fn item_in_file(path: &WorkspaceFilePath, item_name: &str) -> Result<Self, ParseError> {
378        let module = Self::from_workspace_file(path)?;
379        let full_path = format!("{}::{}", module, item_name);
380        Self::parse(&full_path)
381    }
382
383    /// Create a symbol path for a nested item (e.g., impl block item, enum variant)
384    ///
385    /// # Arguments
386    /// - `path`: The file containing the item
387    /// - `segments`: Additional path segments after the module
388    ///
389    /// # Example
390    /// ```ignore
391    /// let path = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace", "my_crate");
392    /// let symbol = SymbolPath::nested_in_file(&path, &["Foo", "new"])?;
393    /// // → my_crate::Foo::new
394    /// ```
395    pub fn nested_in_file(path: &WorkspaceFilePath, segments: &[&str]) -> Result<Self, ParseError> {
396        let module = Self::from_workspace_file(path)?;
397        let mut full_path = module.to_string();
398        for seg in segments {
399            full_path.push_str("::");
400            full_path.push_str(seg);
401        }
402        Self::parse(&full_path)
403    }
404
405    /// Convert a WorkspaceFilePath to its corresponding SymbolPath string representation.
406    ///
407    /// This is the core conversion function from file paths to symbol paths. It handles
408    /// both library and binary entry points, applying the correct naming convention for each.
409    ///
410    /// # Binary Entry Points (main.rs)
411    ///
412    /// Binary entry points use the `main::` prefix to distinguish them from library symbols:
413    ///
414    /// ```ignore
415    /// // For src/main.rs in crate "my_app":
416    /// module_path_str(path) → "main::my_app"
417    ///
418    /// // For src/main.rs with nested symbol:
419    /// // File: src/main.rs with fn main() {}
420    /// module_path_str(path) → "main::my_app"
421    /// // Then the full symbol path becomes: "main::my_app::main"
422    /// ```
423    ///
424    /// This `main::` prefix serves critical purposes:
425    /// 1. **Disambiguation**: Prevents conflicts between lib.rs and main.rs symbols
426    /// 2. **File Resolution**: `RegistryGenerator` uses `is_main_symbol()` to determine
427    ///    whether to output to `src/main.rs` or `src/lib.rs`
428    /// 3. **Bin-Only Detection**: Crates with only main.rs (no lib.rs) work correctly
429    ///    because symbols are explicitly marked as binary symbols
430    ///
431    /// # Library Entry Points (lib.rs)
432    ///
433    /// Library entry points use the crate name directly:
434    ///
435    /// ```ignore
436    /// // For src/lib.rs in crate "my_lib":
437    /// module_path_str(path) → "my_lib"
438    /// ```
439    ///
440    /// # Module Files
441    ///
442    /// Nested module files follow standard Rust module path conventions:
443    ///
444    /// ```ignore
445    /// // For src/models.rs:
446    /// module_path_str(path) → "my_crate::models"
447    ///
448    /// // For src/models/user.rs:
449    /// module_path_str(path) → "my_crate::models::user"
450    /// ```
451    ///
452    /// # Reverse Conversion
453    ///
454    /// The reverse conversion (SymbolPath → WorkspaceFilePath) is handled by:
455    /// - `FilePathResolver::resolve_candidates_with_crate_info()` - Uses CargoMetadataProvider
456    ///   to determine the correct file path, handling both lib and bin targets
457    ///
458    /// # Example Flow (Bin-Only Crate)
459    ///
460    /// ```ignore
461    /// // 1. File Loading (src/main.rs → SymbolPath)
462    /// let path = WorkspaceFilePath::new(..., "src/main.rs", "my_app");
463    /// let symbol_path = SymbolPath::module_path_str(&path);
464    /// // → "main::my_app"
465    ///
466    /// // 2. Symbol Registration
467    /// registry.register(SymbolPath::parse("main::my_app::Status")?, SymbolKind::Enum);
468    ///
469    /// // 3. File Generation (SymbolPath → WorkspaceFilePath)
470    /// let candidates = file_resolver.resolve_candidates_with_crate_info(
471    ///     &SymbolPath::parse("main::my_app::Status")?,
472    ///     &crate_info
473    /// );
474    /// // → ["src/main.rs"] (because is_main_symbol() returns true)
475    /// ```
476    ///
477    /// # See Also
478    ///
479    /// - [`SymbolPath::is_main_symbol()`] - Checks if a symbol is from a binary entry
480    /// - `FilePathResolver::resolve_candidates_with_crate_info()` - Reverse conversion
481    /// - `RegistryGenerator::generate()` - Uses is_main_symbol() to determine output file
482    pub fn module_path_str(path: &WorkspaceFilePath) -> String {
483        let crate_name = path.crate_name().to_module_name();
484        let relative = path.as_relative();
485        let path_str = relative.to_string_lossy();
486
487        // Handle "crates/<name>/src/" pattern - find and skip to src/
488        let work_path = if let Some(src_idx) = path_str.find("/src/") {
489            // Skip everything before and including "src/"
490            &path_str[src_idx + 5..]
491        } else {
492            // Remove "src/" prefix if present at the start
493            path_str.strip_prefix("src/").unwrap_or(&path_str)
494        };
495
496        // Remove ".rs" suffix
497        let without_rs = work_path.strip_suffix(".rs").unwrap_or(work_path);
498
499        // Handle mod.rs - strip the trailing /mod
500        let module_part = without_rs.strip_suffix("/mod").unwrap_or(without_rs);
501
502        // Handle lib.rs and main.rs as crate root
503        if module_part == "lib" {
504            return crate_name;
505        } else if module_part == "main" {
506            // Binary entry: prefix with "main::" to distinguish from library symbols
507            return format!("main::{}", crate_name);
508        }
509
510        // Convert path separators to ::
511        let module_path = module_part.replace('/', "::");
512        format!("{}::{}", crate_name, module_path)
513    }
514
515    // ========== Parsing ==========
516
517    /// Calculate crate root depth from segments
518    ///
519    /// # Returns
520    /// - 2 if first segment is "main" (bin crate: main::crate_name)
521    /// - 1 otherwise (lib crate: crate_name)
522    ///
523    /// # Panics
524    /// - If segments is empty (design invariant violation)
525    /// - If first segment is "main" but no second segment exists
526    fn calculate_crate_root_depth(segments: &[Segment]) -> u8 {
527        assert!(
528            !segments.is_empty(),
529            "SymbolPath segments must not be empty"
530        );
531
532        let first = segments[0].name();
533        if first == "main" {
534            // main:: prefix requires at least 2 segments: main::crate_name
535            if segments.len() < 2 {
536                panic!(
537                    "Invalid main symbol path: 'main' prefix requires crate name (e.g., main::my_app), got: main"
538                );
539            }
540            2
541        } else {
542            1
543        }
544    }
545
546    /// Parse from string representation (e.g., "my_crate::foo::Bar")
547    ///
548    /// Handles `::` inside `<impl ...>` segments correctly by treating
549    /// angle-bracketed blocks as atomic (e.g., `<impl io::Write for Foo>`
550    /// is a single segment, not split on `io::Write`).
551    pub fn parse(s: &str) -> Result<Self, ParseError> {
552        if s.is_empty() {
553            return Err(ParseError::Empty);
554        }
555
556        let segments: Result<SmallVec<[Segment; 12]>, _> = split_respecting_angle_brackets(s)
557            .into_iter()
558            .map(Segment::new)
559            .collect();
560
561        let segments = segments?;
562
563        if segments.is_empty() {
564            return Err(ParseError::Empty);
565        }
566
567        // CRITICAL: Reject semantic keywords (crate, self, super)
568        // SymbolPath must be canonical (e.g., "tokio::net::TcpStream")
569        // Semantic keywords are context-dependent and must be resolved before parsing
570        let first = segments[0].name();
571        if matches!(first, "crate" | "self" | "super") {
572            return Err(ParseError::SemanticKeyword(first.to_string()));
573        }
574
575        // Validate main:: prefix requires crate name
576        if first == "main" && segments.len() < 2 {
577            return Err(ParseError::InvalidIdentifier(
578                "main:: prefix requires crate name (e.g., main::my_app)".to_string(),
579            ));
580        }
581
582        let crate_root_depth = Self::calculate_crate_root_depth(&segments);
583
584        Ok(Self {
585            segments,
586            crate_root_depth,
587        })
588    }
589
590    /// Parse with registry validation - ensures crate name exists in registry.
591    ///
592    /// Use this when constructing paths from user input or DSL where the crate
593    /// name must be validated against known crates.
594    ///
595    /// # Errors
596    /// - `ParseError::UnknownCrate` if the crate name is not in the registry
597    /// - All errors from `parse()`
598    pub fn parse_validated(s: &str, registry: &crate::SymbolRegistry) -> Result<Self, ParseError> {
599        let path = Self::parse(s)?;
600
601        // Check if crate name exists in registry
602        let crate_name = path.crate_name();
603        let crate_exists = registry.iter().any(|(_, p)| p.crate_name() == crate_name);
604
605        if !crate_exists {
606            let known: Vec<_> = registry
607                .iter()
608                .map(|(_, p)| p.crate_name())
609                .collect::<std::collections::HashSet<_>>()
610                .into_iter()
611                .collect();
612            return Err(ParseError::UnknownCrate {
613                path: s.to_string(),
614                crate_name: crate_name.to_string(),
615                known: known.join(", "),
616            });
617        }
618
619        Ok(path)
620    }
621
622    /// Construct from segments (with validation)
623    pub fn from_segments(
624        segments: impl IntoIterator<Item = impl AsRef<str>>,
625    ) -> Result<Self, ParseError> {
626        let segments: SmallVec<[Segment; 12]> = segments
627            .into_iter()
628            .map(|s| Segment::new(s))
629            .collect::<Result<_, _>>()?;
630
631        if segments.is_empty() {
632            return Err(ParseError::Empty);
633        }
634
635        let crate_root_depth = Self::calculate_crate_root_depth(&segments);
636
637        Ok(Self {
638            segments,
639            crate_root_depth,
640        })
641    }
642
643    /// Create a builder for constructing SymbolPath
644    ///
645    /// # Examples
646    /// ```
647    /// # use ryo_symbol::SymbolPath;
648    /// let path = SymbolPath::builder("my_crate")
649    ///     .push("module")
650    ///     .push("MyStruct")
651    ///     .build()
652    ///     .unwrap();
653    /// assert_eq!(path.to_string(), "my_crate::module::MyStruct");
654    /// ```
655    pub fn builder(crate_name: impl Into<String>) -> SymbolPathBuilder {
656        SymbolPathBuilder::new(crate_name)
657    }
658
659    /// Create a path with variable scope (for variable registration)
660    ///
661    /// # Examples
662    /// ```
663    /// # use ryo_symbol::{SymbolPath, VarScope};
664    /// let fn_path = SymbolPath::parse("my_crate::my_fn").unwrap();
665    /// let var_path = fn_path.with_var_scope(VarScope::Param, "x").unwrap();
666    /// assert_eq!(var_path.to_string(), "my_crate::my_fn::$param::x");
667    /// ```
668    pub fn with_var_scope(&self, scope: VarScope, name: &str) -> Result<Self, ParseError> {
669        let mut segments = self.segments.clone();
670        segments.push(Segment::new_unchecked(scope.segment()));
671        segments.push(Segment::new(name)?);
672        Ok(Self {
673            segments,
674            crate_root_depth: self.crate_root_depth, // Inherit from parent
675        })
676    }
677
678    // ========== Accessors ==========
679
680    /// Get the crate name (first segment)
681    #[inline]
682    pub fn crate_name(&self) -> &str {
683        self.segments[0].name()
684    }
685
686    /// Check if this is a main.rs symbol (starts with "main::")
687    ///
688    /// Main symbols use a special prefix to distinguish them from library symbols.
689    /// This allows explicit targeting of binary entry points in Intent DSL.
690    ///
691    /// # Example
692    /// ```ignore
693    /// let main_symbol = SymbolPath::parse("main::my_crate::Config")?;
694    /// assert!(main_symbol.is_main_symbol());
695    ///
696    /// let lib_symbol = SymbolPath::parse("my_crate::Config")?;
697    /// assert!(!lib_symbol.is_main_symbol());
698    /// ```
699    #[inline]
700    pub fn is_main_symbol(&self) -> bool {
701        self.crate_name() == "main"
702    }
703
704    /// Get the actual crate name for main symbols
705    ///
706    /// For paths like "main::my_crate::Foo", returns "my_crate".
707    /// Returns None if this is not a main symbol or has insufficient depth.
708    ///
709    /// # Example
710    /// ```ignore
711    /// let path = SymbolPath::parse("main::my_crate::Config")?;
712    /// assert_eq!(path.main_target_crate(), Some("my_crate"));
713    ///
714    /// let lib_path = SymbolPath::parse("my_crate::Config")?;
715    /// assert_eq!(lib_path.main_target_crate(), None);
716    /// ```
717    pub fn main_target_crate(&self) -> Option<&str> {
718        if self.is_main_symbol() && self.depth() >= 2 {
719            self.segment(1).map(|s| s.name())
720        } else {
721            None
722        }
723    }
724
725    /// Convert a main symbol path to its library equivalent
726    ///
727    /// Strips the "main::" prefix, converting "main::my_crate::Foo" to "my_crate::Foo".
728    /// Returns None if this is not a main symbol.
729    ///
730    /// # Example
731    /// ```ignore
732    /// let main_path = SymbolPath::parse("main::my_crate::Config")?;
733    /// let lib_path = main_path.to_lib_path()?;
734    /// assert_eq!(lib_path.to_string(), "my_crate::Config");
735    /// ```
736    pub fn to_lib_path(&self) -> Option<SymbolPath> {
737        if !self.is_main_symbol() || self.depth() < 2 {
738            return None;
739        }
740        // Skip the "main" prefix and build new path
741        let segments: SmallVec<[Segment; 12]> = self.segments[1..].iter().cloned().collect();
742        Some(Self {
743            segments,
744            crate_root_depth: 1, // lib crate has depth 1
745        })
746    }
747
748    // ========== Crate Root API (New) ==========
749
750    /// Check if this symbol path represents a crate root itself
751    ///
752    /// Returns true if this path is the crate root module (not a submodule).
753    ///
754    /// # Examples
755    /// ```ignore
756    /// let lib_root = SymbolPath::parse("my_lib")?;
757    /// assert!(lib_root.is_crate_root());
758    ///
759    /// let lib_mod = SymbolPath::parse("my_lib::utils")?;
760    /// assert!(!lib_mod.is_crate_root());
761    ///
762    /// let bin_root = SymbolPath::parse("main::my_app")?;
763    /// assert!(bin_root.is_crate_root());
764    ///
765    /// let bin_mod = SymbolPath::parse("main::my_app::utils")?;
766    /// assert!(!bin_mod.is_crate_root());
767    /// ```
768    #[inline]
769    pub fn is_crate_root(&self) -> bool {
770        self.segments.len() == self.crate_root_depth as usize
771    }
772
773    /// Get the actual crate name (excluding main:: prefix)
774    ///
775    /// Returns the real crate name, stripping the "main::" prefix for binary crates.
776    ///
777    /// # Examples
778    /// ```ignore
779    /// let lib_path = SymbolPath::parse("my_lib::Config")?;
780    /// assert_eq!(lib_path.actual_crate_name(), "my_lib");
781    ///
782    /// let bin_path = SymbolPath::parse("main::my_app::Config")?;
783    /// assert_eq!(bin_path.actual_crate_name(), "my_app");
784    /// ```
785    #[inline]
786    pub fn actual_crate_name(&self) -> &str {
787        if self.crate_root_depth == 2 {
788            // main::crate_name → return "crate_name"
789            self.segments[1].name()
790        } else {
791            // crate_name → return "crate_name"
792            self.segments[0].name()
793        }
794    }
795
796    /// Get the module path relative to crate root
797    ///
798    /// Returns segments after the crate root.
799    ///
800    /// # Examples
801    /// ```ignore
802    /// let path = SymbolPath::parse("my_lib::utils::Helper")?;
803    /// let mod_path = path.mod_path();
804    /// assert_eq!(mod_path.len(), 2); // ["utils", "Helper"]
805    ///
806    /// let bin_path = SymbolPath::parse("main::my_app::utils::Helper")?;
807    /// let bin_mod_path = bin_path.mod_path();
808    /// assert_eq!(bin_mod_path.len(), 2); // ["utils", "Helper"]
809    ///
810    /// let root = SymbolPath::parse("my_lib")?;
811    /// assert_eq!(root.mod_path().len(), 0); // []
812    /// ```
813    #[inline]
814    pub fn mod_path(&self) -> &[Segment] {
815        &self.segments[self.crate_root_depth as usize..]
816    }
817
818    /// Get the depth (number of segments)
819    #[inline]
820    pub fn depth(&self) -> usize {
821        self.segments.len()
822    }
823
824    /// Iterate over segment names
825    pub fn segments(&self) -> impl Iterator<Item = &str> {
826        self.segments.iter().map(|s| s.name())
827    }
828
829    /// Get segment references as slice
830    pub fn segment_refs(&self) -> &[Segment] {
831        &self.segments
832    }
833
834    /// Get segment at index
835    pub fn segment(&self, index: usize) -> Option<&Segment> {
836        self.segments.get(index)
837    }
838
839    /// Get the last segment (symbol name)
840    #[inline]
841    pub fn name(&self) -> &str {
842        self.segments.last().map(|s| s.name()).unwrap_or("")
843    }
844
845    // ========== Navigation ==========
846
847    /// Get the parent path
848    ///
849    /// # Examples
850    /// ```ignore
851    /// let path = SymbolPath::parse("tokio::sync::Mutex::lock")?;
852    /// assert_eq!(path.parent().unwrap().to_string(), "tokio::sync::Mutex");
853    /// ```
854    pub fn parent(&self) -> Option<SymbolPath> {
855        if self.segments.len() <= 1 {
856            return None;
857        }
858        let mut segments = self.segments.clone();
859        segments.pop();
860        Some(Self {
861            segments,
862            crate_root_depth: self.crate_root_depth, // Inherit from self
863        })
864    }
865
866    /// Check if this path is an ancestor of another
867    pub fn is_ancestor_of(&self, other: &SymbolPath) -> bool {
868        if self.segments.len() >= other.segments.len() {
869            return false;
870        }
871        self.segments
872            .iter()
873            .zip(other.segments.iter())
874            .all(|(a, b)| a == b)
875    }
876
877    /// Check if this path is a descendant of another
878    pub fn is_descendant_of(&self, other: &SymbolPath) -> bool {
879        other.is_ancestor_of(self)
880    }
881
882    /// Check if both paths are in the same crate
883    #[inline]
884    pub fn same_crate(&self, other: &SymbolPath) -> bool {
885        self.crate_name() == other.crate_name()
886    }
887
888    /// Get the common ancestor of two paths
889    pub fn common_ancestor(&self, other: &SymbolPath) -> Option<SymbolPath> {
890        let mut common: SmallVec<[Segment; 12]> = SmallVec::new();
891
892        for (a, b) in self.segments.iter().zip(other.segments.iter()) {
893            if a == b {
894                common.push(a.clone());
895            } else {
896                break;
897            }
898        }
899
900        if common.is_empty() {
901            None
902        } else {
903            let crate_root_depth = Self::calculate_crate_root_depth(&common);
904            Some(Self {
905                segments: common,
906                crate_root_depth,
907            })
908        }
909    }
910
911    /// Construct a child path by appending a segment.
912    ///
913    /// # Example
914    /// ```
915    /// # use ryo_symbol::SymbolPath;
916    /// let path = SymbolPath::parse("tokio::sync").unwrap();
917    /// let child = path.child("Mutex").unwrap();
918    /// assert_eq!(child.to_string(), "tokio::sync::Mutex");
919    /// ```
920    pub fn child(&self, name: impl AsRef<str>) -> Result<SymbolPath, ParseError> {
921        let segment = Segment::new(name)?;
922        let mut segments = self.segments.clone();
923        segments.push(segment);
924        Ok(SymbolPath {
925            segments,
926            crate_root_depth: self.crate_root_depth, // Inherit from parent
927        })
928    }
929
930    /// Append an inherent impl segment: `parent::<impl Type>`
931    pub fn child_inherent_impl(&self, self_ty: &str) -> SymbolPath {
932        let mut segments = self.segments.clone();
933        segments.push(Segment::inherent_impl(self_ty));
934        SymbolPath {
935            segments,
936            crate_root_depth: self.crate_root_depth,
937        }
938    }
939
940    /// Append a trait impl segment: `parent::<impl Trait for Type>`
941    pub fn child_trait_impl(&self, trait_name: &str, self_ty: &str) -> SymbolPath {
942        let mut segments = self.segments.clone();
943        segments.push(Segment::trait_impl(trait_name, self_ty));
944        SymbolPath {
945            segments,
946            crate_root_depth: self.crate_root_depth,
947        }
948    }
949
950    /// Get the module path (parent of this symbol).
951    ///
952    /// Returns the path without the final item name.
953    /// For `tokio::sync::Mutex::lock`, returns `"tokio::sync::Mutex"`.
954    /// For a crate root like `tokio`, returns `"tokio"`.
955    pub fn module_path(&self) -> String {
956        if self.segments.len() <= 1 {
957            self.crate_name().to_string()
958        } else {
959            self.segments[..self.segments.len() - 1]
960                .iter()
961                .map(|s| s.name())
962                .collect::<Vec<_>>()
963                .join("::")
964        }
965    }
966
967    /// Create a new path with the last segment renamed.
968    ///
969    /// If the last segment matches `from`, it is replaced with `to`.
970    /// Otherwise, returns a clone of self.
971    pub fn with_renamed_last_segment(&self, from: &str, to: &str) -> SymbolPath {
972        if self.name() != from {
973            return self.clone();
974        }
975
976        let mut segments = self.segments.clone();
977        if let Some(last) = segments.last_mut() {
978            *last = Segment::new_unchecked(to);
979        }
980        SymbolPath {
981            segments,
982            crate_root_depth: self.crate_root_depth, // Inherit from self
983        }
984    }
985
986    // ========== InSymbol Support ==========
987
988    /// Check if this is an InSymbol (variable/parameter/field)
989    pub fn is_in_symbol(&self) -> bool {
990        self.segments.iter().any(|s| s.is_scope_marker())
991    }
992
993    /// Get the containing symbol path (for InSymbol)
994    ///
995    /// `my_crate::my_fn::$param::x` → `my_crate::my_fn`
996    pub fn containing_symbol(&self) -> Option<SymbolPath> {
997        let pos = self.segments.iter().position(|s| s.is_scope_marker())?;
998        if pos == 0 {
999            return None;
1000        }
1001        Some(Self {
1002            segments: self.segments[..pos].into(),
1003            crate_root_depth: self.crate_root_depth, // Inherit from self
1004        })
1005    }
1006
1007    /// Get the variable name (for InSymbol)
1008    ///
1009    /// `my_crate::my_fn::$param::x` → `"x"`
1010    pub fn var_name(&self) -> Option<&str> {
1011        if !self.is_in_symbol() {
1012            return None;
1013        }
1014        self.segments.last().map(|s| s.name())
1015    }
1016
1017    /// Get the VarScope if this is an InSymbol path
1018    pub fn var_scope(&self) -> Option<VarScope> {
1019        self.segments
1020            .iter()
1021            .find(|s| s.is_scope_marker())
1022            .and_then(|s| s.as_var_scope())
1023    }
1024
1025    // ========== Body Path Navigation ==========
1026
1027    /// Check if this path contains a `$body` segment
1028    pub fn has_body_segment(&self) -> bool {
1029        self.segments.iter().any(|s| s.is_body_scope())
1030    }
1031
1032    /// Get the position of the `$body` segment
1033    pub fn body_segment_index(&self) -> Option<usize> {
1034        self.segments.iter().position(|s| s.is_body_scope())
1035    }
1036
1037    /// Extract body indices (numeric segments after `$body`)
1038    ///
1039    /// # Example
1040    /// ```
1041    /// # use ryo_symbol::SymbolPath;
1042    /// let path = SymbolPath::parse("my_crate::my_fn::$body::0::1::2").unwrap();
1043    /// assert_eq!(path.body_indices(), Some(vec![0, 1, 2]));
1044    ///
1045    /// let no_body = SymbolPath::parse("my_crate::my_fn").unwrap();
1046    /// assert_eq!(no_body.body_indices(), None);
1047    /// ```
1048    pub fn body_indices(&self) -> Option<Vec<usize>> {
1049        let body_idx = self.body_segment_index()?;
1050        let indices: Option<Vec<usize>> = self.segments[body_idx + 1..]
1051            .iter()
1052            .map(|s| s.tuple_field_index())
1053            .collect();
1054        indices
1055    }
1056
1057    /// Get the function path (everything before `$body`)
1058    ///
1059    /// # Example
1060    /// ```
1061    /// # use ryo_symbol::SymbolPath;
1062    /// let path = SymbolPath::parse("my_crate::my_fn::$body::0::1").unwrap();
1063    /// let fn_path = path.function_path().unwrap();
1064    /// assert_eq!(fn_path.to_string(), "my_crate::my_fn");
1065    /// ```
1066    pub fn function_path(&self) -> Option<SymbolPath> {
1067        let body_idx = self.body_segment_index()?;
1068        if body_idx == 0 {
1069            return None;
1070        }
1071        let segments: SmallVec<[Segment; 12]> = self.segments[..body_idx].iter().cloned().collect();
1072        let crate_root_depth = Self::calculate_crate_root_depth(&segments);
1073        Some(SymbolPath {
1074            segments,
1075            crate_root_depth,
1076        })
1077    }
1078
1079    /// Split into function path and body indices
1080    ///
1081    /// # Returns
1082    /// `Some((function_path, body_indices))` if path contains `$body`, `None` otherwise.
1083    pub fn split_at_body(&self) -> Option<(SymbolPath, Vec<usize>)> {
1084        let fn_path = self.function_path()?;
1085        let indices = self.body_indices()?;
1086        Some((fn_path, indices))
1087    }
1088}
1089
1090impl Display for SymbolPath {
1091    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1092        let path = self
1093            .segments
1094            .iter()
1095            .map(|s| s.name())
1096            .collect::<Vec<_>>()
1097            .join("::");
1098        write!(f, "{}", path)
1099    }
1100}
1101
1102impl FromStr for SymbolPath {
1103    type Err = ParseError;
1104
1105    fn from_str(s: &str) -> Result<Self, Self::Err> {
1106        Self::parse(s)
1107    }
1108}
1109
1110/// Builder for SymbolPath
1111///
1112/// # Validation Strategy
1113/// - `push()`: No validation (performance)
1114/// - `build()`: Validate all segments at once (safety)
1115pub struct SymbolPathBuilder {
1116    segments: SmallVec<[Segment; 12]>,
1117}
1118
1119impl SymbolPathBuilder {
1120    fn new(crate_name: impl Into<String>) -> Self {
1121        let mut segments = SmallVec::new();
1122        segments.push(Segment::new_unchecked(crate_name.into()));
1123        Self { segments }
1124    }
1125
1126    /// Add a segment (no validation until build())
1127    pub fn push(mut self, name: impl AsRef<str>) -> Self {
1128        self.segments.push(Segment::new_unchecked(name));
1129        self
1130    }
1131
1132    /// Build the SymbolPath (validates all segments)
1133    ///
1134    /// # Errors
1135    /// - `ParseError::Empty`: No segments
1136    /// - `ParseError::InvalidIdentifier`: Invalid Rust identifier
1137    pub fn build(self) -> Result<SymbolPath, ParseError> {
1138        if self.segments.is_empty() {
1139            return Err(ParseError::Empty);
1140        }
1141
1142        // Validate all segments
1143        for seg in &self.segments {
1144            validate_rust_identifier(seg.name())?;
1145        }
1146
1147        let crate_root_depth = SymbolPath::calculate_crate_root_depth(&self.segments);
1148
1149        Ok(SymbolPath {
1150            segments: self.segments,
1151            crate_root_depth,
1152        })
1153    }
1154}
1155
1156/// Split a path string by `::`, but treat `<...>` blocks as atomic.
1157///
1158/// Standard `split("::")` breaks paths like `foo::<impl io::Write for Bar>::method`
1159/// because the `::` inside `<impl io::Write ...>` is treated as a separator.
1160/// This function tracks angle bracket depth to avoid splitting inside `<...>`.
1161fn split_respecting_angle_brackets(s: &str) -> Vec<&str> {
1162    let mut segments = Vec::new();
1163    let mut depth = 0u32;
1164    let mut start = 0;
1165    let bytes = s.as_bytes();
1166    let len = bytes.len();
1167    let mut i = 0;
1168
1169    while i < len {
1170        match bytes[i] {
1171            b'<' => {
1172                depth += 1;
1173                i += 1;
1174            }
1175            b'>' => {
1176                depth = depth.saturating_sub(1);
1177                i += 1;
1178            }
1179            b':' if depth == 0 && i + 1 < len && bytes[i + 1] == b':' => {
1180                segments.push(&s[start..i]);
1181                i += 2; // skip "::"
1182                start = i;
1183            }
1184            _ => {
1185                i += 1;
1186            }
1187        }
1188    }
1189
1190    // Push the final segment
1191    if start <= len {
1192        segments.push(&s[start..len]);
1193    }
1194
1195    segments
1196}
1197
1198#[cfg(test)]
1199mod tests {
1200    use super::*;
1201
1202    #[test]
1203    fn test_parse_simple() {
1204        let path = SymbolPath::parse("tokio::sync::Mutex").unwrap();
1205        assert_eq!(path.crate_name(), "tokio");
1206        assert_eq!(path.depth(), 3);
1207        assert_eq!(path.name(), "Mutex");
1208        assert_eq!(path.to_string(), "tokio::sync::Mutex");
1209    }
1210
1211    #[test]
1212    fn test_builder() {
1213        let path = SymbolPath::builder("tokio")
1214            .push("sync")
1215            .push("Mutex")
1216            .push("lock")
1217            .build()
1218            .unwrap();
1219
1220        assert_eq!(path.to_string(), "tokio::sync::Mutex::lock");
1221    }
1222
1223    #[test]
1224    fn test_parent() {
1225        let path = SymbolPath::parse("tokio::sync::Mutex::lock").unwrap();
1226        let parent = path.parent().unwrap();
1227        assert_eq!(parent.to_string(), "tokio::sync::Mutex");
1228
1229        // Crate root has no parent
1230        let crate_path = SymbolPath::parse("tokio").unwrap();
1231        assert!(crate_path.parent().is_none());
1232    }
1233
1234    #[test]
1235    fn test_ancestor() {
1236        let parent = SymbolPath::parse("tokio::sync").unwrap();
1237        let child = SymbolPath::parse("tokio::sync::Mutex::lock").unwrap();
1238
1239        assert!(parent.is_ancestor_of(&child));
1240        assert!(child.is_descendant_of(&parent));
1241        assert!(!child.is_ancestor_of(&parent));
1242    }
1243
1244    #[test]
1245    fn test_common_ancestor() {
1246        let path1 = SymbolPath::parse("tokio::sync::Mutex::lock").unwrap();
1247        let path2 = SymbolPath::parse("tokio::sync::RwLock::read").unwrap();
1248
1249        let common = path1.common_ancestor(&path2).unwrap();
1250        assert_eq!(common.to_string(), "tokio::sync");
1251    }
1252
1253    #[test]
1254    fn test_tuple_field() {
1255        let seg = Segment::tuple_field(0);
1256        assert!(seg.is_tuple_field());
1257        assert_eq!(seg.tuple_field_index(), Some(0));
1258    }
1259
1260    #[test]
1261    fn test_in_symbol() {
1262        let path = SymbolPath::parse("my_crate::my_fn").unwrap();
1263        let var_path = path.with_var_scope(VarScope::Param, "x").unwrap();
1264
1265        assert!(var_path.is_in_symbol());
1266        assert_eq!(var_path.var_name(), Some("x"));
1267        assert_eq!(
1268            var_path.containing_symbol().unwrap().to_string(),
1269            "my_crate::my_fn"
1270        );
1271    }
1272
1273    #[test]
1274    fn test_from_file_path() {
1275        let crate_name = CrateName::new_for_test("my_crate");
1276
1277        // lib.rs -> crate root
1278        let lib_path = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace", "my_crate");
1279        let sym = SymbolPath::from_file_path(&crate_name, &lib_path).unwrap();
1280        assert_eq!(sym.to_string(), "my_crate");
1281
1282        // src/foo/bar.rs -> my_crate::foo::bar
1283        let mod_path = WorkspaceFilePath::new_for_test("src/foo/bar.rs", "/workspace", "my_crate");
1284        let sym = SymbolPath::from_file_path(&crate_name, &mod_path).unwrap();
1285        assert_eq!(sym.to_string(), "my_crate::foo::bar");
1286
1287        // src/foo/mod.rs -> my_crate::foo
1288        let mod_path = WorkspaceFilePath::new_for_test("src/foo/mod.rs", "/workspace", "my_crate");
1289        let sym = SymbolPath::from_file_path(&crate_name, &mod_path).unwrap();
1290        assert_eq!(sym.to_string(), "my_crate::foo");
1291    }
1292
1293    #[test]
1294    fn test_from_workspace_file() {
1295        // Uses embedded crate_name from WorkspaceFilePath
1296        let path = WorkspaceFilePath::new_for_test("src/foo/bar.rs", "/workspace", "my_crate");
1297        let sym = SymbolPath::from_workspace_file(&path).unwrap();
1298        assert_eq!(sym.to_string(), "my_crate::foo::bar");
1299
1300        // lib.rs
1301        let lib_path = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace", "my_crate");
1302        let sym = SymbolPath::from_workspace_file(&lib_path).unwrap();
1303        assert_eq!(sym.to_string(), "my_crate");
1304    }
1305
1306    #[test]
1307    fn test_item_in_file() {
1308        let path = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace", "my_crate");
1309        let sym = SymbolPath::item_in_file(&path, "MyStruct").unwrap();
1310        assert_eq!(sym.to_string(), "my_crate::MyStruct");
1311
1312        let path = WorkspaceFilePath::new_for_test("src/foo/bar.rs", "/workspace", "my_crate");
1313        let sym = SymbolPath::item_in_file(&path, "Baz").unwrap();
1314        assert_eq!(sym.to_string(), "my_crate::foo::bar::Baz");
1315    }
1316
1317    #[test]
1318    fn test_nested_in_file() {
1319        let path = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace", "my_crate");
1320        let sym = SymbolPath::nested_in_file(&path, &["Foo", "new"]).unwrap();
1321        assert_eq!(sym.to_string(), "my_crate::Foo::new");
1322    }
1323
1324    #[test]
1325    fn test_module_path_str() {
1326        let path = WorkspaceFilePath::new_for_test("src/foo/bar.rs", "/workspace", "my_crate");
1327        assert_eq!(SymbolPath::module_path_str(&path), "my_crate::foo::bar");
1328
1329        let lib_path = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace", "my_crate");
1330        assert_eq!(SymbolPath::module_path_str(&lib_path), "my_crate");
1331
1332        let mod_path = WorkspaceFilePath::new_for_test("src/foo/mod.rs", "/workspace", "my_crate");
1333        assert_eq!(SymbolPath::module_path_str(&mod_path), "my_crate::foo");
1334
1335        // Test "crates/<name>/src/" pattern
1336        let workspace_path =
1337            WorkspaceFilePath::new_for_test("crates/mylib/src/lib.rs", "/workspace", "mylib");
1338        assert_eq!(SymbolPath::module_path_str(&workspace_path), "mylib");
1339
1340        let workspace_path2 =
1341            WorkspaceFilePath::new_for_test("crates/mylib/src/foo.rs", "/workspace", "mylib");
1342        assert_eq!(SymbolPath::module_path_str(&workspace_path2), "mylib::foo");
1343    }
1344
1345    #[test]
1346    fn test_from_file_path_workspace_pattern() {
1347        // Test "crates/<name>/src/" pattern
1348        let path =
1349            WorkspaceFilePath::new_for_test("crates/mylib/src/lib.rs", "/workspace", "mylib");
1350        let symbol = SymbolPath::from_workspace_file(&path).unwrap();
1351        assert_eq!(symbol.to_string(), "mylib");
1352
1353        let path2 =
1354            WorkspaceFilePath::new_for_test("crates/mylib/src/foo.rs", "/workspace", "mylib");
1355        let symbol2 = SymbolPath::from_workspace_file(&path2).unwrap();
1356        assert_eq!(symbol2.to_string(), "mylib::foo");
1357
1358        let path3 =
1359            WorkspaceFilePath::new_for_test("crates/mylib/src/foo/bar.rs", "/workspace", "mylib");
1360        let symbol3 = SymbolPath::from_workspace_file(&path3).unwrap();
1361        assert_eq!(symbol3.to_string(), "mylib::foo::bar");
1362    }
1363
1364    #[test]
1365    fn test_main_symbol() {
1366        // Main symbol detection
1367        let main_path = SymbolPath::parse("main::my_crate::Config").unwrap();
1368        assert!(main_path.is_main_symbol());
1369        assert_eq!(main_path.main_target_crate(), Some("my_crate"));
1370
1371        // Library symbol (not main)
1372        let lib_path = SymbolPath::parse("my_crate::Config").unwrap();
1373        assert!(!lib_path.is_main_symbol());
1374        assert_eq!(lib_path.main_target_crate(), None);
1375
1376        // Main symbol with nested path
1377        let nested_main = SymbolPath::parse("main::my_crate::module::Foo").unwrap();
1378        assert!(nested_main.is_main_symbol());
1379        assert_eq!(nested_main.main_target_crate(), Some("my_crate"));
1380
1381        // Just "main" is invalid (main:: prefix requires crate name)
1382        let just_main = SymbolPath::parse("main");
1383        assert!(just_main.is_err());
1384    }
1385
1386    #[test]
1387    fn test_to_lib_path() {
1388        // Convert main symbol to lib path
1389        let main_path = SymbolPath::parse("main::my_crate::Config").unwrap();
1390        let lib_path = main_path.to_lib_path().unwrap();
1391        assert_eq!(lib_path.to_string(), "my_crate::Config");
1392        assert!(!lib_path.is_main_symbol());
1393
1394        // Nested main symbol
1395        let nested = SymbolPath::parse("main::my_crate::module::Foo").unwrap();
1396        let lib = nested.to_lib_path().unwrap();
1397        assert_eq!(lib.to_string(), "my_crate::module::Foo");
1398
1399        // Library path returns None
1400        let lib_path = SymbolPath::parse("my_crate::Config").unwrap();
1401        assert!(lib_path.to_lib_path().is_none());
1402
1403        // Just "main" is invalid (parse will fail)
1404        let just_main = SymbolPath::parse("main");
1405        assert!(just_main.is_err());
1406    }
1407
1408    #[test]
1409    fn test_is_crate_root() {
1410        // Library crate roots
1411        let lib_root = SymbolPath::parse("my_lib").unwrap();
1412        assert!(lib_root.is_crate_root());
1413
1414        let lib_mod = SymbolPath::parse("my_lib::utils").unwrap();
1415        assert!(!lib_mod.is_crate_root());
1416
1417        let lib_nested = SymbolPath::parse("my_lib::utils::Helper").unwrap();
1418        assert!(!lib_nested.is_crate_root());
1419
1420        // Binary crate roots
1421        let bin_root = SymbolPath::parse("main::my_app").unwrap();
1422        assert!(bin_root.is_crate_root());
1423
1424        let bin_mod = SymbolPath::parse("main::my_app::utils").unwrap();
1425        assert!(!bin_mod.is_crate_root());
1426
1427        let bin_nested = SymbolPath::parse("main::my_app::utils::Helper").unwrap();
1428        assert!(!bin_nested.is_crate_root());
1429    }
1430
1431    #[test]
1432    fn test_actual_crate_name() {
1433        // Library crate
1434        let lib_path = SymbolPath::parse("my_lib::Config").unwrap();
1435        assert_eq!(lib_path.actual_crate_name(), "my_lib");
1436
1437        // Binary crate
1438        let bin_path = SymbolPath::parse("main::my_app::Config").unwrap();
1439        assert_eq!(bin_path.actual_crate_name(), "my_app");
1440
1441        // Nested paths
1442        let nested = SymbolPath::parse("main::my_app::utils::Helper").unwrap();
1443        assert_eq!(nested.actual_crate_name(), "my_app");
1444    }
1445
1446    #[test]
1447    fn test_mod_path() {
1448        // Library crate root - no module path
1449        let lib_root = SymbolPath::parse("my_lib").unwrap();
1450        assert_eq!(lib_root.mod_path().len(), 0);
1451
1452        // Library crate with module
1453        let lib_mod = SymbolPath::parse("my_lib::utils").unwrap();
1454        assert_eq!(lib_mod.mod_path().len(), 1);
1455        assert_eq!(lib_mod.mod_path()[0].name(), "utils");
1456
1457        let lib_nested = SymbolPath::parse("my_lib::utils::Helper").unwrap();
1458        assert_eq!(lib_nested.mod_path().len(), 2);
1459        assert_eq!(lib_nested.mod_path()[0].name(), "utils");
1460        assert_eq!(lib_nested.mod_path()[1].name(), "Helper");
1461
1462        // Binary crate root - no module path
1463        let bin_root = SymbolPath::parse("main::my_app").unwrap();
1464        assert_eq!(bin_root.mod_path().len(), 0);
1465
1466        // Binary crate with module
1467        let bin_mod = SymbolPath::parse("main::my_app::utils").unwrap();
1468        assert_eq!(bin_mod.mod_path().len(), 1);
1469        assert_eq!(bin_mod.mod_path()[0].name(), "utils");
1470
1471        let bin_nested = SymbolPath::parse("main::my_app::utils::Helper").unwrap();
1472        assert_eq!(bin_nested.mod_path().len(), 2);
1473        assert_eq!(bin_nested.mod_path()[0].name(), "utils");
1474        assert_eq!(bin_nested.mod_path()[1].name(), "Helper");
1475    }
1476
1477    #[test]
1478    fn test_reject_semantic_keywords() {
1479        // SymbolPath must be canonical - semantic keywords are forbidden
1480
1481        // "crate" is not allowed (use actual crate name)
1482        let result = SymbolPath::parse("crate");
1483        assert!(matches!(result, Err(ParseError::SemanticKeyword(_))));
1484        assert!(
1485            result
1486                .unwrap_err()
1487                .to_string()
1488                .contains("semantic keyword 'crate' not allowed"),
1489            "Error message should mention semantic keyword"
1490        );
1491
1492        // "crate::foo" is not allowed
1493        let result = SymbolPath::parse("crate::foo");
1494        assert!(matches!(result, Err(ParseError::SemanticKeyword(_))));
1495
1496        // "self" is not allowed
1497        let result = SymbolPath::parse("self");
1498        assert!(matches!(result, Err(ParseError::SemanticKeyword(_))));
1499
1500        // "self::foo" is not allowed
1501        let result = SymbolPath::parse("self::foo");
1502        assert!(matches!(result, Err(ParseError::SemanticKeyword(_))));
1503
1504        // "super" is not allowed
1505        let result = SymbolPath::parse("super");
1506        assert!(matches!(result, Err(ParseError::SemanticKeyword(_))));
1507
1508        // "super::foo" is not allowed
1509        let result = SymbolPath::parse("super::foo");
1510        assert!(matches!(result, Err(ParseError::SemanticKeyword(_))));
1511
1512        // Valid canonical paths are OK
1513        assert!(SymbolPath::parse("my_crate").is_ok());
1514        assert!(SymbolPath::parse("my_crate::foo").is_ok());
1515        assert!(SymbolPath::parse("tokio::net::TcpStream").is_ok());
1516    }
1517
1518    // ========== Impl Segment Tests ==========
1519
1520    #[test]
1521    fn test_segment_inherent_impl() {
1522        let seg = Segment::inherent_impl("Counter");
1523        assert_eq!(seg.name(), "<impl Counter>");
1524        assert!(seg.is_impl());
1525        assert!(!seg.is_trait_impl());
1526        assert_eq!(seg.impl_self_ty(), Some("Counter"));
1527        assert_eq!(seg.impl_trait(), None);
1528    }
1529
1530    #[test]
1531    fn test_segment_trait_impl() {
1532        let seg = Segment::trait_impl("Display", "Counter");
1533        assert_eq!(seg.name(), "<impl Display for Counter>");
1534        assert!(seg.is_impl());
1535        assert!(seg.is_trait_impl());
1536        assert_eq!(seg.impl_self_ty(), Some("Counter"));
1537        assert_eq!(seg.impl_trait(), Some("Display"));
1538    }
1539
1540    #[test]
1541    fn test_segment_non_impl() {
1542        let seg = Segment::new("foo").unwrap();
1543        assert!(!seg.is_impl());
1544        assert!(!seg.is_trait_impl());
1545        assert_eq!(seg.impl_self_ty(), None);
1546        assert_eq!(seg.impl_trait(), None);
1547    }
1548
1549    #[test]
1550    fn test_segment_impl_validation_roundtrip() {
1551        // Constructed impl segments must pass validation
1552        let inherent = Segment::inherent_impl("MyStruct");
1553        assert!(Segment::new(inherent.name()).is_ok());
1554
1555        let trait_impl = Segment::trait_impl("Clone", "MyStruct");
1556        assert!(Segment::new(trait_impl.name()).is_ok());
1557    }
1558
1559    #[test]
1560    fn test_symbolpath_child_inherent_impl() {
1561        let module = SymbolPath::parse("my_crate::module").unwrap();
1562        let impl_path = module.child_inherent_impl("TodoList");
1563        assert_eq!(impl_path.to_string(), "my_crate::module::<impl TodoList>");
1564        assert_eq!(impl_path.depth(), 3);
1565
1566        // Method under impl's parent type
1567        let method = module.child("TodoList").unwrap().child("new").unwrap();
1568        assert_eq!(method.to_string(), "my_crate::module::TodoList::new");
1569    }
1570
1571    #[test]
1572    fn test_symbolpath_child_trait_impl() {
1573        let module = SymbolPath::parse("my_crate::module").unwrap();
1574        let impl_path = module.child_trait_impl("Display", "TodoList");
1575        assert_eq!(
1576            impl_path.to_string(),
1577            "my_crate::module::<impl Display for TodoList>"
1578        );
1579
1580        // Method under trait impl
1581        let method = impl_path.child("fmt").unwrap();
1582        assert_eq!(
1583            method.to_string(),
1584            "my_crate::module::<impl Display for TodoList>::fmt"
1585        );
1586    }
1587
1588    #[test]
1589    fn test_symbolpath_impl_parse_roundtrip() {
1590        // Display → parse roundtrip
1591        let module = SymbolPath::parse("my_crate").unwrap();
1592        let impl_path = module.child_inherent_impl("Foo");
1593        let reparsed = SymbolPath::parse(&impl_path.to_string()).unwrap();
1594        assert_eq!(impl_path, reparsed);
1595
1596        let trait_path = module.child_trait_impl("Bar", "Foo");
1597        let reparsed = SymbolPath::parse(&trait_path.to_string()).unwrap();
1598        assert_eq!(trait_path, reparsed);
1599    }
1600
1601    #[test]
1602    fn test_impl_segment_last_segment_inspection() {
1603        let path = SymbolPath::parse("my_crate::module").unwrap();
1604        let impl_path = path.child_trait_impl("Iterator", "MyIter");
1605
1606        let last = impl_path.segment(impl_path.depth() - 1).unwrap();
1607        assert!(last.is_impl());
1608        assert!(last.is_trait_impl());
1609        assert_eq!(last.impl_self_ty(), Some("MyIter"));
1610        assert_eq!(last.impl_trait(), Some("Iterator"));
1611    }
1612
1613    #[test]
1614    fn test_impl_parent_navigation() {
1615        let path = SymbolPath::parse("my_crate").unwrap();
1616        let impl_path = path.child_trait_impl("Display", "Config");
1617        let method = impl_path.child("fmt").unwrap();
1618
1619        // parent of method → impl path
1620        let parent = method.parent().unwrap();
1621        assert_eq!(parent, impl_path);
1622
1623        // parent of impl → module
1624        let grandparent = parent.parent().unwrap();
1625        assert_eq!(grandparent, path);
1626    }
1627
1628    #[test]
1629    fn test_parse_roundtrip_qualified_trait_impl() {
1630        // BUG: SymbolPath::parse splits on "::" naively, breaking qualified
1631        // trait names like "io::Write" inside <impl io::Write for Writer<'_>>.
1632        // This causes ASTRegistry to fail lookup for methods of such impls.
1633        let built = SymbolPath::parse("axum::helpers")
1634            .unwrap()
1635            .child_trait_impl("io::Write", "Writer < '_ >");
1636        let method = built.child("write").unwrap();
1637
1638        // to_string should include the qualified trait name
1639        let method_str = method.to_string();
1640        assert_eq!(
1641            method_str,
1642            "axum::helpers::<impl io::Write for Writer < '_ >>::write"
1643        );
1644
1645        // parse(to_string) must round-trip correctly
1646        let reparsed = SymbolPath::parse(&method_str)
1647            .expect("parse must handle :: inside <impl ...> segments");
1648        assert_eq!(reparsed, method, "round-trip must preserve segments");
1649        assert_eq!(reparsed.name(), "write");
1650        assert_eq!(reparsed.depth(), 4); // axum, helpers, <impl ...>, write
1651    }
1652}