Skip to main content

disqualified/
short_name.rs

1/// Lazily shortens a type name to remove all module paths.
2///
3/// The short name of a type is its full name as returned by
4/// [`core::any::type_name`], but with the prefix of all paths removed. For
5/// example, the short name of `alloc::vec::Vec<core::option::Option<u32>>`
6/// would be `Vec<Option<u32>>`.
7///
8/// Shortening is performed lazily without allocation.
9#[cfg_attr(
10    feature = "alloc",
11    doc = r#" To get a [`String`] from this type, use the [`to_string`](`alloc::string::ToString::to_string`) method."#
12)]
13///
14/// # Examples
15///
16/// ```rust
17/// # use disqualified::ShortName;
18/// #
19/// # mod foo {
20/// #     pub mod bar {
21/// #         pub struct Baz;
22/// #     }
23/// # }
24/// let short_name = ShortName::of::<foo::bar::Baz>(); // Baz
25/// ```
26#[derive(Clone, Copy)]
27pub struct ShortName<'a>(pub &'a str);
28
29impl ShortName<'static> {
30    /// Gets a shortened version of the name of the type `T`.
31    pub fn of<T: ?Sized>() -> Self {
32        Self(core::any::type_name::<T>())
33    }
34}
35
36impl<'a> ShortName<'a> {
37    /// Gets the original name before shortening.
38    pub const fn original(&self) -> &'a str {
39        self.0
40    }
41}
42
43impl<'a> From<&'a str> for ShortName<'a> {
44    fn from(value: &'a str) -> Self {
45        Self(value)
46    }
47}
48
49impl<'a> core::fmt::Debug for ShortName<'a> {
50    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
51        let &ShortName(full_name) = self;
52        // Generics result in nested paths within <..> blocks.
53        // Consider "bevy_render::camera::camera::extract_cameras<bevy_render::camera::bundle::Camera3d>".
54        // To tackle this, we parse the string from left to right, collapsing as we go.
55        let mut index: usize = 0;
56        let end_of_string = full_name.len();
57
58        while index < end_of_string {
59            let rest_of_string = full_name.get(index..end_of_string).unwrap_or_default();
60
61            // Collapse everything up to the next special character,
62            // then skip over it
63            if let Some(special_character_index) = rest_of_string.find(|c: char| {
64                (c == ' ')
65                    || (c == '<')
66                    || (c == '>')
67                    || (c == '(')
68                    || (c == ')')
69                    || (c == '[')
70                    || (c == ']')
71                    || (c == ',')
72                    || (c == ';')
73                    || (c == '&')
74            }) {
75                let segment_to_collapse = rest_of_string
76                    .get(0..special_character_index)
77                    .unwrap_or_default();
78
79                f.write_str(collapse_type_name(segment_to_collapse))?;
80
81                // Insert the special character
82                let special_character =
83                    &rest_of_string[special_character_index..=special_character_index];
84
85                if special_character == "<" {
86                    let mut end_lifetime_index = special_character_index + 1;
87                    // Elide lifetime parameters, which always come first in generics.
88                    while rest_of_string[end_lifetime_index..].starts_with("'_, ") {
89                        end_lifetime_index += 4;
90                    }
91                    if rest_of_string[end_lifetime_index..].starts_with("'_>") {
92                        // If all parameters are lifetime parameters,
93                        // also elide the angle brackets.
94                        end_lifetime_index += 3;
95                    } else {
96                        f.write_str("<")?;
97                    }
98                    index += end_lifetime_index;
99                    continue;
100                }
101
102                f.write_str(special_character)?;
103
104                match special_character {
105                    ">" | ")" | "]"
106                        if rest_of_string[special_character_index + 1..].starts_with("::") =>
107                    {
108                        f.write_str("::")?;
109                        // Move the index past the "::"
110                        index += special_character_index + 3;
111                    }
112                    // Move the index just past the special character
113                    _ => index += special_character_index + 1,
114                }
115            } else {
116                // If there are no special characters left, we're done!
117                f.write_str(collapse_type_name(rest_of_string))?;
118                index = end_of_string;
119            }
120        }
121
122        Ok(())
123    }
124}
125
126impl<'a> core::fmt::Display for ShortName<'a> {
127    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
128        <Self as core::fmt::Debug>::fmt(self, f)
129    }
130}
131
132#[inline(always)]
133fn collapse_type_name(string: &str) -> &str {
134    // Closures end with some number of `::{{closure}}`,
135    // `::{{call_once}}`, or `::{{call_mut}` segments.
136    // Take only the leftmost, if one exists.
137    let mut closure_start = string.len();
138    let mut closure_end = closure_start;
139    if let Some(end_index) = string.find("}}") {
140        if let Some(start_index) = string[..end_index].find("::{{") {
141            closure_end = end_index + 2;
142            closure_start = start_index;
143        }
144    }
145
146    // Trim modules, but not type names.
147    // If the next to last segment is uppercase, assume it is a type.
148    // This covers both enum variants and methods.
149    let mut segments = string[..closure_start].rsplit("::");
150    let last = segments.next().unwrap();
151    let second_last = segments.next();
152    let type_len = second_last
153        .filter(|second_last| second_last.starts_with(char::is_uppercase))
154        .map_or(0, |second_last| second_last.len() + 2);
155    let index = closure_start - last.len() - type_len;
156    &string[index..closure_end]
157}
158
159#[cfg(all(test, feature = "alloc"))]
160mod name_formatting_tests {
161    use super::ShortName;
162
163    #[test]
164    fn trivial() {
165        assert_eq!(ShortName("test_system").to_string(), "test_system");
166    }
167
168    #[test]
169    fn empty() {
170        assert_eq!(ShortName("").to_string(), "");
171        assert_eq!(ShortName("::").to_string(), "");
172    }
173
174    #[test]
175    fn path_separated() {
176        assert_eq!(
177            ShortName("bevy_prelude::make_fun_game").to_string(),
178            "make_fun_game"
179        );
180    }
181
182    #[test]
183    fn tuple_type() {
184        assert_eq!(
185            ShortName("(String, String)").to_string(),
186            "(String, String)"
187        );
188    }
189
190    #[test]
191    fn array_type() {
192        assert_eq!(ShortName("[i32; 3]").to_string(), "[i32; 3]");
193    }
194
195    #[test]
196    fn trivial_generics() {
197        assert_eq!(ShortName("a<B>").to_string(), "a<B>");
198    }
199
200    #[test]
201    fn multiple_type_parameters() {
202        assert_eq!(ShortName("a<B, C>").to_string(), "a<B, C>");
203    }
204
205    #[test]
206    fn enums() {
207        assert_eq!(ShortName("Option::None").to_string(), "Option::None");
208        assert_eq!(ShortName("Option::Some(2)").to_string(), "Option::Some(2)");
209        assert_eq!(
210            ShortName("bevy_render::RenderSet::Prepare").to_string(),
211            "RenderSet::Prepare"
212        );
213    }
214
215    #[test]
216    fn generics() {
217        assert_eq!(
218            ShortName("bevy_render::camera::camera::extract_cameras<bevy_render::camera::bundle::Camera3d>").to_string(),
219            "extract_cameras<Camera3d>"
220        );
221    }
222
223    #[test]
224    fn nested_generics() {
225        assert_eq!(
226            ShortName("bevy::mad_science::do_mad_science<mad_science::Test<mad_science::Tube>, bavy::TypeSystemAbuse>").to_string(),
227            "do_mad_science<Test<Tube>, TypeSystemAbuse>"
228        );
229    }
230
231    #[test]
232    fn sub_path_after_closing_bracket() {
233        assert_eq!(
234            ShortName("bevy_asset::assets::Assets<bevy_scene::dynamic_scene::DynamicScene>::asset_event_system").to_string(),
235            "Assets<DynamicScene>::asset_event_system"
236        );
237        assert_eq!(
238            ShortName("(String, String)::default").to_string(),
239            "(String, String)::default"
240        );
241        assert_eq!(
242            ShortName("[i32; 16]::default").to_string(),
243            "[i32; 16]::default"
244        );
245    }
246
247    #[test]
248    fn references() {
249        assert_eq!(ShortName("&T").to_string(), "&T");
250        assert_eq!(ShortName("&mut T").to_string(), "&mut T");
251        assert_eq!(ShortName("&t::T<&u::U>").to_string(), "&T<&U>");
252        assert_eq!(
253            ShortName("&mut t::T<&mut u::U>").to_string(),
254            "&mut T<&mut U>"
255        );
256    }
257
258    #[test]
259    fn pointers() {
260        assert_eq!(ShortName("*const T").to_string(), "*const T");
261        assert_eq!(ShortName("*mut T").to_string(), "*mut T");
262        assert_eq!(
263            ShortName("*const t::T<*const u::U>").to_string(),
264            "*const T<*const U>"
265        );
266        assert_eq!(
267            ShortName("*mut t::T<*mut u::U>").to_string(),
268            "*mut T<*mut U>"
269        );
270    }
271
272    #[test]
273    fn lifetimes() {
274        assert_eq!(ShortName("t::T<'_>").to_string(), "T");
275        assert_eq!(ShortName("t::T<'_, '_>").to_string(), "T");
276        assert_eq!(ShortName("t::T<'_, '_, '_>").to_string(), "T");
277        assert_eq!(ShortName("t::T<'_, u::U>").to_string(), "T<U>");
278        assert_eq!(ShortName("t::T<'_, '_, u::U>").to_string(), "T<U>");
279        assert_eq!(ShortName("t::T<'_, '_, '_, u::U>").to_string(), "T<U>");
280    }
281
282    #[test]
283    fn functions() {
284        assert_eq!(ShortName("m::f").to_string(), "f");
285        assert_eq!(ShortName("m::f::{{closure}}").to_string(), "f::{{closure}}");
286        assert_eq!(ShortName("m::Ty::f").to_string(), "Ty::f");
287        assert_eq!(
288            ShortName("m::Ty::f::{{closure}}").to_string(),
289            "Ty::f::{{closure}}"
290        );
291        assert_eq!(ShortName("m::Ty<t::T>::f").to_string(), "Ty<T>::f");
292        assert_eq!(
293            ShortName("m::Ty<t::T>::f::{{closure}}").to_string(),
294            "Ty<T>::f::{{closure}}"
295        );
296
297        assert_eq!(ShortName("m::f<t::T>").to_string(), "f<T>");
298        assert_eq!(
299            ShortName("m::f<t::T>::{{closure}}").to_string(),
300            "f<T>::{{closure}}"
301        );
302        assert_eq!(ShortName("m::Ty::f<t::T>").to_string(), "Ty::f<T>");
303        assert_eq!(
304            ShortName("m::Ty::f<t::T>::{{closure}}").to_string(),
305            "Ty::f<T>::{{closure}}"
306        );
307        assert_eq!(ShortName("m::Ty<t::T>::f<t::T>").to_string(), "Ty<T>::f<T>");
308        assert_eq!(
309            ShortName("m::Ty<t::T>::f<t::T>::{{closure}}").to_string(),
310            "Ty<T>::f<T>::{{closure}}"
311        );
312
313        assert_eq!(
314            ShortName("m::f::{{closure}}::{{closure}}").to_string(),
315            "f::{{closure}}"
316        );
317        assert_eq!(
318            ShortName("m::f::{{closure}}::{{closure}}::{{call_once}}").to_string(),
319            "f::{{closure}}"
320        );
321        assert_eq!(
322            ShortName("m::Ty::f::{{closure}}::{{closure}}").to_string(),
323            "Ty::f::{{closure}}"
324        );
325        assert_eq!(
326            ShortName("m::Ty::f::{{closure}}::{{closure}}::{{call_once}}").to_string(),
327            "Ty::f::{{closure}}"
328        );
329    }
330}