libgir/codegen/doc/
gi_docgen.rs

1use std::{
2    fmt::{self, Display, Formatter},
3    str::FromStr,
4    sync::OnceLock,
5};
6
7use regex::{Captures, Regex};
8
9use super::format::find_method_or_function;
10use crate::{
11    analysis::object::LocationInObject,
12    codegen::doc::format::{
13        gen_alias_doc_link, gen_callback_doc_link, gen_const_doc_link, gen_object_fn_doc_link,
14        gen_property_doc_link, gen_signal_doc_link, gen_symbol_doc_link, gen_vfunc_doc_link,
15    },
16    library::{TypeId, MAIN_NAMESPACE},
17    nameutil::mangle_keywords,
18    Env,
19};
20
21#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
22pub enum GiDocgenError {
23    InvalidLinkType(String),
24    BrokenLinkType(String),
25    InvalidLink,
26}
27
28impl Display for GiDocgenError {
29    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
30        match self {
31            Self::InvalidLinkType(e) => f.write_str(&format!("Invalid link type \"{e}\"")),
32            Self::BrokenLinkType(e) => f.write_str(&format!("Broken link syntax for type \"{e}\"")),
33            Self::InvalidLink => f.write_str("Invalid link syntax"),
34        }
35    }
36}
37
38impl std::error::Error for GiDocgenError {}
39
40/// Convert a "Namespace.Type" to (Option<Namespace>, Type)
41fn namespace_type_from_details(
42    link_details: &str,
43    link_type: &str,
44) -> Result<(Option<String>, String), GiDocgenError> {
45    let res: Vec<&str> = link_details.split('.').collect();
46    let len = res.len();
47    if len == 1 {
48        Ok((None, res[0].to_string()))
49    } else if len == 2 {
50        if res[1].is_empty() {
51            Err(GiDocgenError::BrokenLinkType(link_type.to_string()))
52        } else {
53            Ok((Some(res[0].to_string()), res[1].to_string()))
54        }
55    } else {
56        Err(GiDocgenError::BrokenLinkType(link_type.to_string()))
57    }
58}
59
60/// Convert a "Namespace.Type.method_name" to (Option<Namespace>, Option<Type>,
61/// name) Type is only optional for global functions and the order can be
62/// modified the `is_global_func` parameters
63fn namespace_type_method_from_details(
64    link_details: &str,
65    link_type: &str,
66    is_global_func: bool,
67) -> Result<(Option<String>, Option<String>, String), GiDocgenError> {
68    let res: Vec<&str> = link_details.split('.').collect();
69    let len = res.len();
70    if len == 1 {
71        Ok((None, None, res[0].to_string()))
72    } else if len == 2 {
73        if res[1].is_empty() {
74            Err(GiDocgenError::BrokenLinkType(link_type.to_string()))
75        } else if is_global_func {
76            Ok((Some(res[0].to_string()), None, res[1].to_string()))
77        } else {
78            Ok((None, Some(res[0].to_string()), res[1].to_string()))
79        }
80    } else if len == 3 {
81        if res[2].is_empty() {
82            Err(GiDocgenError::BrokenLinkType(link_type.to_string()))
83        } else {
84            Ok((
85                Some(res[0].to_string()),
86                Some(res[1].to_string()),
87                res[2].to_string(),
88            ))
89        }
90    } else {
91        Err(GiDocgenError::BrokenLinkType(link_type.to_string()))
92    }
93}
94
95fn gi_docgen_symbols() -> &'static Regex {
96    static REGEX: OnceLock<Regex> = OnceLock::new();
97    REGEX.get_or_init(|| {
98        Regex::new(r"\[(callback|id|alias|class|const|ctor|enum|error|flags|func|iface|method|property|signal|struct|vfunc)[@](\w+\b)([:.]+[\w-]+\b)?([:.]+[\w-]+\b)?\]?").unwrap()
99    })
100}
101
102pub(crate) fn replace_c_types(
103    entry: &str,
104    env: &Env,
105    in_type: Option<(&TypeId, Option<LocationInObject>)>,
106) -> String {
107    gi_docgen_symbols()
108        .replace_all(entry, |caps: &Captures<'_>| {
109            if let Ok(gi_type) = GiDocgen::from_str(&caps[0]) {
110                gi_type.rust_link(env, in_type)
111            } else {
112                // otherwise fallback to the original string
113                caps[0].to_string()
114            }
115        })
116        .to_string()
117}
118
119/// A representation of the various ways to link items using GI-docgen
120///
121/// See <https://gnome.pages.gitlab.gnome.org/gi-docgen/linking.html> for details.
122#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
123pub enum GiDocgen {
124    // C-identifier
125    Id(String),
126    // Alias to another type
127    Alias(String),
128    // Object Class
129    Class {
130        namespace: Option<String>,
131        type_: String,
132    },
133    Const {
134        namespace: Option<String>,
135        type_: String,
136    },
137    Constructor {
138        namespace: Option<String>,
139        type_: String,
140        name: String,
141    },
142    Callback {
143        namespace: Option<String>,
144        name: String,
145    },
146    Enum {
147        namespace: Option<String>,
148        type_: String,
149    },
150    Error {
151        namespace: Option<String>,
152        type_: String,
153    },
154    Flag {
155        namespace: Option<String>,
156        type_: String,
157    },
158    Func {
159        namespace: Option<String>,
160        type_: Option<String>,
161        name: String,
162    },
163    Interface {
164        namespace: Option<String>,
165        type_: String,
166    },
167    Method {
168        namespace: Option<String>,
169        type_: String,
170        name: String,
171        is_class_method: bool, // Whether `type_` ends with Class
172    },
173    Property {
174        namespace: Option<String>,
175        type_: String,
176        name: String,
177    },
178    Signal {
179        namespace: Option<String>,
180        type_: String,
181        name: String,
182    },
183    Struct {
184        namespace: Option<String>,
185        type_: String,
186    },
187    VFunc {
188        namespace: Option<String>,
189        type_: String,
190        name: String,
191    },
192}
193
194fn ns_type_to_doc(namespace: &Option<String>, type_: &str) -> String {
195    if let Some(ns) = namespace {
196        format!("{ns}::{type_}")
197    } else {
198        type_.to_string()
199    }
200}
201
202fn find_virtual_method_by_name(
203    type_: Option<&str>,
204    namespace: Option<&str>,
205    name: &str,
206    env: &Env,
207    in_type: Option<(&TypeId, Option<LocationInObject>)>,
208) -> Option<String> {
209    find_method_or_function(
210        env,
211        in_type,
212        |f| {
213            f.name == mangle_keywords(name)
214                && namespace.as_ref().map_or(f.ns_id == MAIN_NAMESPACE, |n| {
215                    &env.library.namespaces[f.ns_id as usize].name == n
216                })
217        },
218        |o| type_.is_none_or(|t| o.name == t && is_same_namespace(env, namespace, o.type_id)),
219        |_| false,
220        |_| false,
221        |_| false,
222        false,
223        true,
224    )
225}
226
227fn find_method_or_function_by_name(
228    type_: Option<&str>,
229    namespace: Option<&str>,
230    name: &str,
231    env: &Env,
232    in_type: Option<(&TypeId, Option<LocationInObject>)>,
233    is_class_method: bool,
234) -> Option<String> {
235    find_method_or_function(
236        env,
237        in_type,
238        |f| {
239            f.name == mangle_keywords(name)
240                && namespace.as_ref().map_or(f.ns_id == MAIN_NAMESPACE, |n| {
241                    &env.library.namespaces[f.ns_id as usize].name == n
242                })
243        },
244        |o| type_.is_none_or(|t| o.name == t && is_same_namespace(env, namespace, o.type_id)),
245        |r| type_.is_none_or(|t| r.name == t && is_same_namespace(env, namespace, r.type_id)),
246        |e| type_.is_none_or(|t| e.name == t && is_same_namespace(env, namespace, e.type_id)),
247        |f| type_.is_none_or(|t| f.name == t && is_same_namespace(env, namespace, f.type_id)),
248        is_class_method,
249        false,
250    )
251}
252
253fn is_same_namespace(env: &Env, namespace: Option<&str>, type_id: TypeId) -> bool {
254    namespace
255        .as_ref()
256        .map_or(MAIN_NAMESPACE == type_id.ns_id, |n| {
257            &env.library.namespaces[type_id.ns_id as usize].name == n
258        })
259}
260
261impl GiDocgen {
262    pub fn rust_link(
263        &self,
264        env: &Env,
265        in_type: Option<(&TypeId, Option<LocationInObject>)>,
266    ) -> String {
267        let symbols = env.symbols.borrow();
268        match self {
269            GiDocgen::Enum { type_, namespace } | GiDocgen::Error { type_, namespace } => env
270                .analysis
271                .enumerations
272                .iter()
273                .find(|e| &e.name == type_)
274                .map_or_else(
275                    || format!("`{}`", ns_type_to_doc(namespace, type_)),
276                    |info| gen_symbol_doc_link(info.type_id, env),
277                ),
278            GiDocgen::Class { type_, namespace } | GiDocgen::Interface { type_, namespace } => env
279                .analysis
280                .objects
281                .values()
282                .find(|o| {
283                    &o.name == type_ && is_same_namespace(env, namespace.as_deref(), o.type_id)
284                })
285                .map_or_else(
286                    || format!("`{}`", ns_type_to_doc(namespace, type_)),
287                    |info| gen_symbol_doc_link(info.type_id, env),
288                ),
289            GiDocgen::Flag { type_, namespace } => env
290                .analysis
291                .flags
292                .iter()
293                .find(|e| {
294                    &e.name == type_ && is_same_namespace(env, namespace.as_deref(), e.type_id)
295                })
296                .map_or_else(
297                    || format!("`{}`", ns_type_to_doc(namespace, type_)),
298                    |info| gen_symbol_doc_link(info.type_id, env),
299                ),
300            GiDocgen::Const { type_, namespace } => env
301                .analysis
302                .constants
303                .iter()
304                .find(|c| &c.name == type_ && is_same_namespace(env, namespace.as_deref(), c.typ))
305                .map_or_else(
306                    || format!("`{}`", ns_type_to_doc(namespace, type_)),
307                    gen_const_doc_link,
308                ),
309            GiDocgen::Property {
310                type_,
311                name,
312                namespace,
313            } => env
314                .analysis
315                .objects
316                .values()
317                .find(|o| {
318                    &o.name == type_ && is_same_namespace(env, namespace.as_deref(), o.type_id)
319                })
320                .map_or_else(
321                    || gen_property_doc_link(&ns_type_to_doc(namespace, type_), name),
322                    |info| {
323                        let sym = symbols.by_tid(info.type_id).unwrap();
324                        gen_property_doc_link(&sym.full_rust_name(), name)
325                    },
326                ),
327            GiDocgen::Signal {
328                type_,
329                name,
330                namespace,
331            } => env
332                .analysis
333                .objects
334                .values()
335                .find(|o| {
336                    &o.name == type_ && is_same_namespace(env, namespace.as_deref(), o.type_id)
337                })
338                .map_or_else(
339                    || gen_signal_doc_link(&ns_type_to_doc(namespace, type_), name),
340                    |info| {
341                        let sym = symbols.by_tid(info.type_id).unwrap();
342                        gen_signal_doc_link(&sym.full_rust_name(), name)
343                    },
344                ),
345            GiDocgen::Id(c_name) => symbols.by_c_name(c_name).map_or_else(
346                || format!("`{c_name}`"),
347                |sym| format!("[`{n}`][crate::{n}]", n = sym.full_rust_name()),
348            ),
349            GiDocgen::Struct { namespace, type_ } => env
350                .analysis
351                .records
352                .values()
353                .find(|r| {
354                    &r.name == type_ && is_same_namespace(env, namespace.as_deref(), r.type_id)
355                })
356                .map_or_else(
357                    || format!("`{}`", ns_type_to_doc(namespace, type_)),
358                    |info| gen_symbol_doc_link(info.type_id, env),
359                ),
360            GiDocgen::Constructor {
361                namespace,
362                type_,
363                name,
364            } => env
365                .analysis
366                .find_object_by_function(
367                    env,
368                    |o| &o.name == type_ && is_same_namespace(env, namespace.as_deref(), o.type_id),
369                    |f| f.name == mangle_keywords(name),
370                )
371                .map_or_else(
372                    || format!("`{}::{}()`", ns_type_to_doc(namespace, type_), name),
373                    |(obj_info, fn_info)| {
374                        gen_object_fn_doc_link(obj_info, fn_info, env, in_type, type_)
375                    },
376                ),
377            GiDocgen::Func {
378                namespace,
379                type_,
380                name,
381            } => find_method_or_function_by_name(
382                type_.as_deref(),
383                namespace.as_deref(),
384                name,
385                env,
386                in_type,
387                false,
388            )
389            .unwrap_or_else(|| {
390                if let Some(ty) = type_ {
391                    format!("`{}::{}()`", ns_type_to_doc(namespace, ty), name)
392                } else {
393                    format!("`{name}()`")
394                }
395            }),
396            GiDocgen::Alias(alias) => gen_alias_doc_link(alias),
397            GiDocgen::Method {
398                namespace,
399                type_,
400                name,
401                is_class_method,
402            } => find_method_or_function_by_name(
403                Some(type_),
404                namespace.as_deref(),
405                name,
406                env,
407                in_type,
408                *is_class_method,
409            )
410            .unwrap_or_else(|| format!("`{}::{}()`", ns_type_to_doc(namespace, type_), name)),
411            GiDocgen::Callback { namespace, name } => {
412                gen_callback_doc_link(&ns_type_to_doc(namespace, name))
413            }
414            GiDocgen::VFunc {
415                namespace,
416                type_,
417                name,
418            } => find_virtual_method_by_name(Some(type_), namespace.as_deref(), name, env, in_type)
419                .unwrap_or_else(|| gen_vfunc_doc_link(&ns_type_to_doc(namespace, type_), name)),
420        }
421    }
422}
423
424impl FromStr for GiDocgen {
425    type Err = GiDocgenError;
426    // We assume the string is contained inside a []
427    fn from_str(item_link: &str) -> Result<Self, Self::Err> {
428        let item_link = item_link.trim_start_matches('[').trim_end_matches(']');
429        if let Some((link_type, link_details)) = item_link.split_once('@') {
430            match link_type {
431                "alias" => Ok(Self::Alias(link_details.to_string())),
432                "class" => {
433                    let (namespace, type_) = namespace_type_from_details(link_details, "class")?;
434                    Ok(Self::Class { namespace, type_ })
435                }
436                "const" => {
437                    let (namespace, type_) = namespace_type_from_details(link_details, "const")?;
438                    Ok(Self::Const { namespace, type_ })
439                }
440                "ctor" => {
441                    let (namespace, type_, name) =
442                        namespace_type_method_from_details(link_details, "ctor", false)?;
443                    Ok(Self::Constructor {
444                        namespace,
445                        type_: type_
446                            .ok_or_else(|| GiDocgenError::BrokenLinkType("ctor".to_string()))?,
447                        name,
448                    })
449                }
450                "enum" => {
451                    let (namespace, type_) = namespace_type_from_details(link_details, "enum")?;
452                    Ok(Self::Enum { namespace, type_ })
453                }
454                "error" => {
455                    let (namespace, type_) = namespace_type_from_details(link_details, "error")?;
456                    Ok(Self::Error { namespace, type_ })
457                }
458                "flags" => {
459                    let (namespace, type_) = namespace_type_from_details(link_details, "flags")?;
460                    Ok(Self::Flag { namespace, type_ })
461                }
462                "func" => {
463                    let (namespace, type_, name) =
464                        namespace_type_method_from_details(link_details, "func", true)?;
465                    Ok(Self::Func {
466                        namespace,
467                        type_,
468                        name,
469                    })
470                }
471                "iface" => {
472                    let (namespace, type_) = namespace_type_from_details(link_details, "iface")?;
473                    Ok(Self::Interface { namespace, type_ })
474                }
475                "callback" => {
476                    let (namespace, name) = namespace_type_from_details(link_details, "callback")?;
477                    Ok(Self::Callback { namespace, name })
478                }
479                "method" => {
480                    let (namespace, type_, name) =
481                        namespace_type_method_from_details(link_details, "method", false)?;
482                    let type_ =
483                        type_.ok_or_else(|| GiDocgenError::BrokenLinkType("method".to_string()))?;
484                    Ok(Self::Method {
485                        namespace,
486                        is_class_method: type_.ends_with("Class"),
487                        type_,
488                        name,
489                    })
490                }
491                "property" => {
492                    let (namespace, type_) = namespace_type_from_details(link_details, "property")?;
493                    let type_details: Vec<_> = type_.split(':').collect();
494                    if type_details.len() < 2 || type_details[1].is_empty() {
495                        Err(GiDocgenError::BrokenLinkType("property".to_string()))
496                    } else {
497                        Ok(Self::Property {
498                            namespace,
499                            type_: type_details[0].to_string(),
500                            name: type_details[1].to_string(),
501                        })
502                    }
503                }
504                "signal" => {
505                    let (namespace, type_) = namespace_type_from_details(link_details, "signal")?;
506                    let type_details: Vec<_> = type_.split("::").collect();
507                    if type_details.len() < 2 || type_details[1].is_empty() {
508                        Err(GiDocgenError::BrokenLinkType("signal".to_string()))
509                    } else {
510                        Ok(Self::Signal {
511                            namespace,
512                            type_: type_details[0].to_string(),
513                            name: type_details[1].to_string(),
514                        })
515                    }
516                }
517                "struct" => {
518                    let (namespace, type_) = namespace_type_from_details(link_details, "struct")?;
519                    Ok(Self::Struct { namespace, type_ })
520                }
521                "vfunc" => {
522                    let (namespace, type_, name) =
523                        namespace_type_method_from_details(link_details, "vfunc", false)?;
524                    Ok(Self::VFunc {
525                        namespace,
526                        type_: type_
527                            .ok_or_else(|| GiDocgenError::BrokenLinkType("vfunc".to_string()))?,
528                        name,
529                    })
530                }
531                "id" => Ok(Self::Id(link_details.to_string())),
532                e => Err(GiDocgenError::InvalidLinkType(e.to_string())),
533            }
534        } else {
535            Err(GiDocgenError::InvalidLink)
536        }
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    #[test]
544    fn test_link_alias() {
545        assert_eq!(
546            GiDocgen::from_str("[alias@Allocation]"),
547            Ok(GiDocgen::Alias("Allocation".to_string()))
548        );
549    }
550
551    #[test]
552    fn test_link_class() {
553        assert_eq!(
554            GiDocgen::from_str("[class@Widget]"),
555            Ok(GiDocgen::Class {
556                namespace: None,
557                type_: "Widget".to_string(),
558            })
559        );
560        assert_eq!(
561            GiDocgen::from_str("[class@Gdk.Surface]"),
562            Ok(GiDocgen::Class {
563                namespace: Some("Gdk".to_string()),
564                type_: "Surface".to_string(),
565            })
566        );
567        assert_eq!(
568            GiDocgen::from_str("[class@Gsk.RenderNode]"),
569            Ok(GiDocgen::Class {
570                namespace: Some("Gsk".to_string()),
571                type_: "RenderNode".to_string(),
572            })
573        );
574
575        assert_eq!(
576            GiDocgen::from_str("[class@Gsk.RenderNode.test]"),
577            Err(GiDocgenError::BrokenLinkType("class".to_string()))
578        );
579
580        assert_eq!(
581            GiDocgen::from_str("[class@Gsk.]"),
582            Err(GiDocgenError::BrokenLinkType("class".to_string()))
583        );
584    }
585
586    #[test]
587    fn test_link_id() {
588        assert_eq!(
589            GiDocgen::from_str("[id@gtk_widget_show]"),
590            Ok(GiDocgen::Id("gtk_widget_show".to_string()))
591        );
592    }
593
594    #[test]
595    fn test_link_const() {
596        assert_eq!(
597            GiDocgen::from_str("[const@Gdk.KEY_q]"),
598            Ok(GiDocgen::Const {
599                namespace: Some("Gdk".to_string()),
600                type_: "KEY_q".to_string()
601            })
602        );
603    }
604
605    #[test]
606    fn test_link_callback() {
607        assert_eq!(
608            GiDocgen::from_str("[callback@Gtk.MapListModelMapFunc]"),
609            Ok(GiDocgen::Callback {
610                namespace: Some("Gtk".to_string()),
611                name: "MapListModelMapFunc".to_string()
612            })
613        );
614    }
615
616    #[test]
617    fn test_link_enum() {
618        assert_eq!(
619            GiDocgen::from_str("[enum@Orientation]"),
620            Ok(GiDocgen::Enum {
621                namespace: None,
622                type_: "Orientation".to_string()
623            })
624        );
625    }
626
627    #[test]
628    fn test_link_error() {
629        assert_eq!(
630            GiDocgen::from_str("[error@Gtk.BuilderParseError]"),
631            Ok(GiDocgen::Error {
632                namespace: Some("Gtk".to_string()),
633                type_: "BuilderParseError".to_string()
634            })
635        );
636    }
637
638    #[test]
639    fn test_link_flags() {
640        assert_eq!(
641            GiDocgen::from_str("[flags@Gdk.ModifierType]"),
642            Ok(GiDocgen::Flag {
643                namespace: Some("Gdk".to_string()),
644                type_: "ModifierType".to_string()
645            })
646        );
647    }
648
649    #[test]
650    fn test_link_iface() {
651        assert_eq!(
652            GiDocgen::from_str("[iface@Gtk.Buildable]"),
653            Ok(GiDocgen::Interface {
654                namespace: Some("Gtk".to_string()),
655                type_: "Buildable".to_string()
656            })
657        );
658    }
659
660    #[test]
661    fn test_link_struct() {
662        assert_eq!(
663            GiDocgen::from_str("[struct@Gtk.TextIter]"),
664            Ok(GiDocgen::Struct {
665                namespace: Some("Gtk".to_string()),
666                type_: "TextIter".to_string()
667            })
668        );
669    }
670
671    #[test]
672    fn test_link_property() {
673        assert_eq!(
674            GiDocgen::from_str("[property@Gtk.Orientable:orientation]"),
675            Ok(GiDocgen::Property {
676                namespace: Some("Gtk".to_string()),
677                type_: "Orientable".to_string(),
678                name: "orientation".to_string(),
679            })
680        );
681
682        assert_eq!(
683            GiDocgen::from_str("[property@Gtk.Orientable]"),
684            Err(GiDocgenError::BrokenLinkType("property".to_string()))
685        );
686
687        assert_eq!(
688            GiDocgen::from_str("[property@Gtk.Orientable:]"),
689            Err(GiDocgenError::BrokenLinkType("property".to_string()))
690        );
691    }
692
693    #[test]
694    fn test_link_signal() {
695        assert_eq!(
696            GiDocgen::from_str("[signal@Gtk.RecentManager::changed]"),
697            Ok(GiDocgen::Signal {
698                namespace: Some("Gtk".to_string()),
699                type_: "RecentManager".to_string(),
700                name: "changed".to_string(),
701            })
702        );
703
704        assert_eq!(
705            GiDocgen::from_str("[signal@Gtk.RecentManager]"),
706            Err(GiDocgenError::BrokenLinkType("signal".to_string()))
707        );
708
709        assert_eq!(
710            GiDocgen::from_str("[signal@Gtk.RecentManager::]"),
711            Err(GiDocgenError::BrokenLinkType("signal".to_string()))
712        );
713
714        assert_eq!(
715            GiDocgen::from_str("[signal@Gtk.RecentManager:]"),
716            Err(GiDocgenError::BrokenLinkType("signal".to_string()))
717        );
718    }
719
720    #[test]
721    fn test_link_vfunc() {
722        assert_eq!(
723            GiDocgen::from_str("[vfunc@Gtk.Widget.measure]"),
724            Ok(GiDocgen::VFunc {
725                namespace: Some("Gtk".to_string()),
726                type_: "Widget".to_string(),
727                name: "measure".to_string(),
728            })
729        );
730
731        assert_eq!(
732            GiDocgen::from_str("[vfunc@Widget.snapshot]"),
733            Ok(GiDocgen::VFunc {
734                namespace: None,
735                type_: "Widget".to_string(),
736                name: "snapshot".to_string(),
737            })
738        );
739    }
740
741    #[test]
742    fn test_link_ctor() {
743        assert_eq!(
744            GiDocgen::from_str("[ctor@Gtk.Box.new]"),
745            Ok(GiDocgen::Constructor {
746                namespace: Some("Gtk".to_string()),
747                type_: "Box".to_string(),
748                name: "new".to_string(),
749            })
750        );
751
752        assert_eq!(
753            GiDocgen::from_str("[ctor@Button.new_with_label]"),
754            Ok(GiDocgen::Constructor {
755                namespace: None,
756                type_: "Button".to_string(),
757                name: "new_with_label".to_string(),
758            })
759        );
760    }
761
762    #[test]
763    fn test_link_func() {
764        assert_eq!(
765            GiDocgen::from_str("[func@Gtk.init]"),
766            Ok(GiDocgen::Func {
767                namespace: Some("Gtk".to_string()),
768                type_: None,
769                name: "init".to_string(),
770            })
771        );
772
773        assert_eq!(
774            GiDocgen::from_str("[func@show_uri]"),
775            Ok(GiDocgen::Func {
776                namespace: None,
777                type_: None,
778                name: "show_uri".to_string(),
779            })
780        );
781
782        assert_eq!(
783            GiDocgen::from_str("[func@Gtk.Window.list_toplevels]"),
784            Ok(GiDocgen::Func {
785                namespace: Some("Gtk".to_string()),
786                type_: Some("Window".to_string()),
787                name: "list_toplevels".to_string(),
788            })
789        );
790    }
791
792    #[test]
793    fn test_link_method() {
794        assert_eq!(
795            GiDocgen::from_str("[method@Gtk.Widget.show]"),
796            Ok(GiDocgen::Method {
797                namespace: Some("Gtk".to_string()),
798                type_: "Widget".to_string(),
799                name: "show".to_string(),
800                is_class_method: false,
801            })
802        );
803
804        assert_eq!(
805            GiDocgen::from_str("[method@WidgetClass.add_binding]"),
806            Ok(GiDocgen::Method {
807                namespace: None,
808                type_: "WidgetClass".to_string(),
809                name: "add_binding".to_string(),
810                is_class_method: true,
811            })
812        );
813    }
814}