pyo3_stub_gen/docgen/
util.rs

1//! Utility functions for documentation generation
2
3/// Module for prefix stripping utilities
4pub mod prefix_stripper {
5    /// Strip standard library prefixes from type expressions
6    ///
7    /// Removes prefixes like "typing.", "builtins.", "collections.abc.", etc.
8    /// while preserving the structure of complex type expressions.
9    pub fn strip_stdlib_prefixes(type_expr: &str) -> String {
10        let stdlib_prefixes = &[
11            "typing.",
12            "builtins.",
13            "collections.abc.",
14            "typing_extensions.",
15            "decimal.",
16            "datetime.",
17            "pathlib.",
18        ];
19
20        let mut result = String::new();
21        let mut i = 0;
22        let chars: Vec<char> = type_expr.chars().collect();
23
24        while i < chars.len() {
25            // Check if we're at the start of a qualified name
26            if i == 0
27                || !chars[i - 1].is_alphanumeric() && chars[i - 1] != '_' && chars[i - 1] != '.'
28            {
29                let remaining: String = chars[i..].iter().collect();
30                let mut matched = false;
31
32                // Try to match stdlib prefixes
33                for prefix in stdlib_prefixes {
34                    if remaining.starts_with(prefix) {
35                        let after_prefix_idx = prefix.len();
36                        if after_prefix_idx < remaining.len() {
37                            let next_char = remaining.chars().nth(after_prefix_idx).unwrap();
38                            if next_char.is_alphabetic() || next_char == '_' {
39                                i += prefix.len();
40                                matched = true;
41                                break;
42                            }
43                        }
44                    }
45                }
46
47                if matched {
48                    continue;
49                }
50            }
51
52            result.push(chars[i]);
53            i += 1;
54        }
55
56        result
57    }
58
59    /// Strip package-qualified names from type expressions
60    ///
61    /// Converts "package.Type" -> "Type" based on heuristics:
62    /// - First part starts with lowercase (likely a module)
63    /// - Last part starts with uppercase (likely a type)
64    pub fn strip_package_prefixes(type_expr: &str, _current_module: &str) -> String {
65        let mut result = String::new();
66        let mut i = 0;
67        let chars: Vec<char> = type_expr.chars().collect();
68
69        while i < chars.len() {
70            // Check if we're at the start of a qualified name
71            if i == 0
72                || !chars[i - 1].is_alphanumeric() && chars[i - 1] != '_' && chars[i - 1] != '.'
73            {
74                let remaining: String = chars[i..].iter().collect();
75
76                // Extract qualified identifier (e.g., "main_mod.A" or "pure.DataContainer")
77                let ident_match = remaining
78                    .split(|c: char| !c.is_alphanumeric() && c != '_' && c != '.')
79                    .next();
80
81                if let Some(ident) = ident_match {
82                    if ident.contains('.') {
83                        // This is a qualified name, check if it's a package.Type pattern
84                        let parts: Vec<&str> = ident.split('.').collect();
85                        if parts.len() >= 2 {
86                            let first_part = parts[0];
87                            let last_part = parts[parts.len() - 1];
88
89                            // If it looks like a package.Type pattern, extract just the Type
90                            if first_part
91                                .chars()
92                                .next()
93                                .map(|c| c.is_lowercase())
94                                .unwrap_or(false)
95                                && last_part
96                                    .chars()
97                                    .next()
98                                    .map(|c| c.is_uppercase())
99                                    .unwrap_or(false)
100                            {
101                                // Skip to the last part
102                                let prefix_len = ident.len() - last_part.len();
103                                i += prefix_len;
104                                continue;
105                            }
106                        }
107                    }
108                }
109            }
110
111            result.push(chars[i]);
112            i += 1;
113        }
114
115        result
116    }
117
118    /// Strip internal module prefixes (modules starting with underscore)
119    ///
120    /// Converts "_core.Type" -> "Type", "_internal._nested.Type" -> "Type"
121    pub fn strip_internal_prefixes(expr: &str) -> String {
122        let mut result = String::new();
123        let mut i = 0;
124        let chars: Vec<char> = expr.chars().collect();
125
126        while i < chars.len() {
127            // Check if we're at the start of a potential module prefix
128            if i == 0 || !chars[i - 1].is_alphanumeric() && chars[i - 1] != '_' {
129                // Try to match a pattern like "_module." or "_module.submodule."
130                let remaining: String = chars[i..].iter().collect();
131
132                // Look for pattern: _identifier followed by .
133                if remaining.starts_with('_') {
134                    // Find the next non-identifier character
135                    let mut j = i + 1;
136                    while j < chars.len() && (chars[j].is_alphanumeric() || chars[j] == '_') {
137                        j += 1;
138                    }
139
140                    // If followed by a dot, this is a module prefix to strip
141                    if j < chars.len() && chars[j] == '.' {
142                        // Skip the module name and the dot
143                        i = j + 1;
144                        continue;
145                    }
146                }
147            }
148
149            result.push(chars[i]);
150            i += 1;
151        }
152
153        result
154    }
155
156    /// Combined stripper for type expressions (stdlib + package prefixes)
157    ///
158    /// This is the main entry point for stripping prefixes from type expressions.
159    pub fn strip_type_prefixes(type_expr: &str, current_module: &str) -> String {
160        let without_stdlib = strip_stdlib_prefixes(type_expr);
161        strip_package_prefixes(&without_stdlib, current_module)
162    }
163
164    /// Simple prefix stripper for default values
165    ///
166    /// Uses simple string replacement for common prefixes.
167    /// This is faster but less sophisticated than strip_type_prefixes.
168    pub fn strip_default_value_prefixes(text: &str) -> String {
169        text.replace("_core.", "")
170            .replace("typing.", "")
171            .replace("builtins.", "")
172    }
173}
174
175/// Check if module is hidden (any path component starts with '_')
176pub fn is_hidden_module(module_name: &str) -> bool {
177    module_name.split('.').any(|part| part.starts_with('_'))
178}
179
180#[cfg(test)]
181mod tests {
182    use super::prefix_stripper::*;
183    use super::*;
184
185    #[test]
186    fn test_strip_stdlib_prefixes() {
187        assert_eq!(strip_stdlib_prefixes("typing.Optional"), "Optional");
188        assert_eq!(strip_stdlib_prefixes("builtins.str"), "str");
189        assert_eq!(
190            strip_stdlib_prefixes("collections.abc.Callable"),
191            "Callable"
192        );
193        assert_eq!(
194            strip_stdlib_prefixes("typing.Optional[typing.List[int]]"),
195            "Optional[List[int]]"
196        );
197    }
198
199    #[test]
200    fn test_strip_package_prefixes() {
201        assert_eq!(strip_package_prefixes("main_mod.ClassA", ""), "ClassA");
202        assert_eq!(
203            strip_package_prefixes("pure.DataContainer", ""),
204            "DataContainer"
205        );
206        assert_eq!(
207            strip_package_prefixes("Optional[main_mod.ClassA]", ""),
208            "Optional[ClassA]"
209        );
210    }
211
212    #[test]
213    fn test_strip_internal_prefixes() {
214        assert_eq!(
215            strip_internal_prefixes("_core.InternalType"),
216            "InternalType"
217        );
218        assert_eq!(strip_internal_prefixes("_internal._nested.Type"), "Type");
219        assert_eq!(
220            strip_internal_prefixes("Optional[_core.Type]"),
221            "Optional[Type]"
222        );
223    }
224
225    #[test]
226    fn test_strip_type_prefixes() {
227        assert_eq!(
228            strip_type_prefixes("typing.Optional[main_mod.ClassA]", "main_mod"),
229            "Optional[ClassA]"
230        );
231    }
232
233    #[test]
234    fn test_strip_default_value_prefixes() {
235        assert_eq!(strip_default_value_prefixes("_core.Foo"), "Foo");
236        assert_eq!(strip_default_value_prefixes("typing.Optional"), "Optional");
237        assert_eq!(strip_default_value_prefixes("builtins.None"), "None");
238    }
239
240    #[test]
241    fn test_is_hidden_module() {
242        assert!(is_hidden_module("_core"));
243        assert!(is_hidden_module("package._internal"));
244        assert!(is_hidden_module("_hidden.submodule"));
245        assert!(!is_hidden_module("public"));
246        assert!(!is_hidden_module("package.submodule"));
247    }
248}