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/// Extract the public parent module name from a module path.
181///
182/// If the module path contains hidden components (starting with '_'),
183/// returns the path up to (but not including) the first hidden component.
184/// If no hidden components exist, returns the original name.
185///
186/// # Examples
187/// - `"generate_init_py._core"` → `"generate_init_py"`
188/// - `"pkg._internal.core"` → `"pkg"`
189/// - `"pkg.mod._hidden"` → `"pkg.mod"`
190/// - `"pkg.mod"` → `"pkg.mod"` (unchanged)
191/// - `"_hidden"` → `""` (empty, entire module is hidden)
192pub fn public_parent_module(module_name: &str) -> String {
193    let public_parts: Vec<&str> = module_name
194        .split('.')
195        .take_while(|part| !part.starts_with('_'))
196        .collect();
197    public_parts.join(".")
198}
199
200#[cfg(test)]
201mod tests {
202    use super::prefix_stripper::*;
203    use super::*;
204
205    #[test]
206    fn test_strip_stdlib_prefixes() {
207        assert_eq!(strip_stdlib_prefixes("typing.Optional"), "Optional");
208        assert_eq!(strip_stdlib_prefixes("builtins.str"), "str");
209        assert_eq!(
210            strip_stdlib_prefixes("collections.abc.Callable"),
211            "Callable"
212        );
213        assert_eq!(
214            strip_stdlib_prefixes("typing.Optional[typing.List[int]]"),
215            "Optional[List[int]]"
216        );
217    }
218
219    #[test]
220    fn test_strip_package_prefixes() {
221        assert_eq!(strip_package_prefixes("main_mod.ClassA", ""), "ClassA");
222        assert_eq!(
223            strip_package_prefixes("pure.DataContainer", ""),
224            "DataContainer"
225        );
226        assert_eq!(
227            strip_package_prefixes("Optional[main_mod.ClassA]", ""),
228            "Optional[ClassA]"
229        );
230    }
231
232    #[test]
233    fn test_strip_internal_prefixes() {
234        assert_eq!(
235            strip_internal_prefixes("_core.InternalType"),
236            "InternalType"
237        );
238        assert_eq!(strip_internal_prefixes("_internal._nested.Type"), "Type");
239        assert_eq!(
240            strip_internal_prefixes("Optional[_core.Type]"),
241            "Optional[Type]"
242        );
243    }
244
245    #[test]
246    fn test_strip_type_prefixes() {
247        assert_eq!(
248            strip_type_prefixes("typing.Optional[main_mod.ClassA]", "main_mod"),
249            "Optional[ClassA]"
250        );
251    }
252
253    #[test]
254    fn test_strip_default_value_prefixes() {
255        assert_eq!(strip_default_value_prefixes("_core.Foo"), "Foo");
256        assert_eq!(strip_default_value_prefixes("typing.Optional"), "Optional");
257        assert_eq!(strip_default_value_prefixes("builtins.None"), "None");
258    }
259
260    #[test]
261    fn test_is_hidden_module() {
262        assert!(is_hidden_module("_core"));
263        assert!(is_hidden_module("package._internal"));
264        assert!(is_hidden_module("_hidden.submodule"));
265        assert!(!is_hidden_module("public"));
266        assert!(!is_hidden_module("package.submodule"));
267    }
268
269    #[test]
270    fn test_public_parent_module() {
271        assert_eq!(
272            public_parent_module("generate_init_py._core"),
273            "generate_init_py"
274        );
275        assert_eq!(public_parent_module("pkg._internal.core"), "pkg");
276        assert_eq!(public_parent_module("pkg.mod._hidden"), "pkg.mod");
277        assert_eq!(public_parent_module("pkg.mod"), "pkg.mod");
278        assert_eq!(public_parent_module("_hidden"), "");
279    }
280}