Skip to main content

gobject_ast/model/
doc.rs

1use std::{fmt, str::FromStr};
2
3use serde::Serialize;
4use tree_sitter::Node;
5
6use super::Comment;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9pub struct Version {
10    pub major: u32,
11    pub minor: u32,
12}
13
14impl fmt::Display for Version {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        write!(f, "{}.{}", self.major, self.minor)
17    }
18}
19
20impl FromStr for Version {
21    type Err = ();
22
23    fn from_str(s: &str) -> Result<Self, Self::Err> {
24        let (major, minor) = s.split_once('.').ok_or(())?;
25        Ok(Self {
26            major: major.parse().map_err(|_| ())?,
27            minor: minor.parse().map_err(|_| ())?,
28        })
29    }
30}
31
32#[derive(Debug, Clone, Serialize)]
33#[serde(rename_all = "snake_case")]
34pub enum ExportMacro {
35    AvailableIn(Version),
36    DeprecatedIn(Version),
37    DeprecatedInFor(Version, String),
38    Other(String),
39}
40
41impl ExportMacro {
42    pub fn parse(name: &str) -> Self {
43        let (name, args) = match name.split_once('(') {
44            Some((n, rest)) => (n.trim(), Some(rest.trim_end_matches(')').trim())),
45            None => (name, None),
46        };
47
48        if let Some(suffix) = name
49            .split("AVAILABLE_IN_")
50            .nth(1)
51            .or_else(|| name.split("AVAILABLE_ENUMERATOR_IN_").nth(1))
52        {
53            if let Some(ver) = Self::parse_version_suffix(suffix) {
54                return Self::AvailableIn(ver);
55            }
56        } else if let Some(suffix) = name
57            .split("DEPRECATED_IN_")
58            .nth(1)
59            .or_else(|| name.split("DEPRECATED_ENUMERATOR_IN_").nth(1))
60            && let Some(ver) = Self::parse_version_suffix(suffix)
61        {
62            if let Some(replacement) = args {
63                return Self::DeprecatedInFor(ver, replacement.to_owned());
64            }
65            return Self::DeprecatedIn(ver);
66        }
67        Self::Other(name.to_owned())
68    }
69
70    fn parse_version_suffix(suffix: &str) -> Option<Version> {
71        let (major, rest) = suffix.split_once('_')?;
72        let minor_len = rest.len() - rest.trim_start_matches(|c: char| c.is_ascii_digit()).len();
73        let minor = &rest[..minor_len];
74        Some(Version {
75            major: major.parse().ok()?,
76            minor: minor.parse().ok()?,
77        })
78    }
79
80    pub fn version(&self) -> Option<&Version> {
81        match self {
82            Self::AvailableIn(v) | Self::DeprecatedIn(v) | Self::DeprecatedInFor(v, _) => Some(v),
83            Self::Other(_) => None,
84        }
85    }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
89#[serde(rename_all = "kebab-case")]
90pub enum TransferKind {
91    None,
92    Full,
93    Container,
94    Floating,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
98#[serde(rename_all = "kebab-case")]
99pub enum ScopeKind {
100    Call,
101    Async,
102    Notified,
103    Forever,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
107pub struct ArrayAnnotation {
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub length: Option<String>,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub fixed_size: Option<u32>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub zero_terminated: Option<bool>,
114}
115
116/// Annotations valid on function parameters.
117#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
118#[serde(rename_all = "kebab-case")]
119pub enum ParamAnnotation {
120    Transfer(TransferKind),
121    Nullable,
122    NotNullable,
123    Optional,
124    AllowNone,
125    NotOptional,
126    In,
127    Out,
128    OutCallerAllocates,
129    OutCalleeAllocates,
130    Inout,
131    Array,
132    ArrayDetailed(ArrayAnnotation),
133    ElementType(Vec<String>),
134    Scope(ScopeKind),
135    Closure,
136    ClosureFor(String),
137    Destroy(String),
138    Type(String),
139    Skip,
140    Default(String),
141    Attributes(Vec<(String, String)>),
142    Unknown(String),
143}
144
145impl ParamAnnotation {
146    pub fn parse(name: &str, value: Option<&str>) -> Self {
147        match name {
148            "transfer" => match parse_transfer(value) {
149                Ok(t) => Self::Transfer(t),
150                Err(e) => {
151                    tracing::warn!("doc: {e}");
152                    Self::Unknown(format_annotation(name, value))
153                }
154            },
155            "nullable" => Self::Nullable,
156            "not nullable" => Self::NotNullable,
157            "optional" => Self::Optional,
158            "allow-none" => Self::AllowNone,
159            "not optional" => Self::NotOptional,
160            "caller-allocates" => Self::OutCallerAllocates,
161            "callee-allocates" => Self::OutCalleeAllocates,
162            "in" => Self::In,
163            "out" => match value {
164                None => Self::Out,
165                Some("caller-allocates") => Self::OutCallerAllocates,
166                Some("callee-allocates") => Self::OutCalleeAllocates,
167                Some(v) => {
168                    tracing::warn!("doc: unknown out modifier: {v:?}");
169                    Self::Unknown(format_annotation(name, value))
170                }
171            },
172            "inout" | "in-out" => Self::Inout,
173            "array" => match value {
174                Some(v) => Self::ArrayDetailed(parse_array(v)),
175                None => Self::Array,
176            },
177            "element-type" => match value {
178                Some(v) => Self::ElementType(v.split_whitespace().map(String::from).collect()),
179                None => {
180                    tracing::warn!("doc: element-type requires at least one type");
181                    Self::Unknown(format_annotation(name, value))
182                }
183            },
184            "scope" => match parse_scope(value) {
185                Ok(s) => Self::Scope(s),
186                Err(e) => {
187                    tracing::warn!("doc: {e}");
188                    Self::Unknown(format_annotation(name, value))
189                }
190            },
191            "closure" => match value {
192                Some(v) => Self::ClosureFor(v.to_owned()),
193                None => Self::Closure,
194            },
195            "destroy" => match value {
196                Some(v) => Self::Destroy(v.to_owned()),
197                None => {
198                    tracing::warn!("doc: destroy requires a parameter name");
199                    Self::Unknown(format_annotation(name, value))
200                }
201            },
202            "type" => match value {
203                Some(v) => Self::Type(v.to_owned()),
204                None => {
205                    tracing::warn!("doc: type requires a type name");
206                    Self::Unknown(format_annotation(name, value))
207                }
208            },
209            "skip" => Self::Skip,
210            "attributes" => Self::Attributes(parse_attributes(value)),
211            "default" => match value {
212                Some(v) => Self::Default(v.to_owned()),
213                None => {
214                    tracing::warn!("doc: default requires a value");
215                    Self::Unknown(format_annotation(name, value))
216                }
217            },
218            _ => {
219                tracing::warn!(
220                    "doc: unknown param annotation: ({})",
221                    format_annotation(name, value)
222                );
223                Self::Unknown(format_annotation(name, value))
224            }
225        }
226    }
227}
228
229/// Annotations valid on return values.
230#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
231#[serde(rename_all = "kebab-case")]
232pub enum ReturnAnnotation {
233    Transfer(TransferKind),
234    Nullable,
235    NotNullable,
236    Optional,
237    Skip,
238    Array,
239    ArrayDetailed(ArrayAnnotation),
240    ElementType(Vec<String>),
241    Type(String),
242    Attributes(Vec<(String, String)>),
243    Unknown(String),
244}
245
246impl ReturnAnnotation {
247    pub fn parse(name: &str, value: Option<&str>) -> Self {
248        match name {
249            "transfer" => match parse_transfer(value) {
250                Ok(t) => Self::Transfer(t),
251                Err(e) => {
252                    tracing::warn!("doc: {e}");
253                    Self::Unknown(format_annotation(name, value))
254                }
255            },
256            "nullable" => Self::Nullable,
257            "not nullable" => Self::NotNullable,
258            "optional" => Self::Optional,
259            "skip" => Self::Skip,
260            "array" => match value {
261                Some(v) => Self::ArrayDetailed(parse_array(v)),
262                None => Self::Array,
263            },
264            "element-type" => match value {
265                Some(v) => Self::ElementType(v.split_whitespace().map(String::from).collect()),
266                None => {
267                    tracing::warn!("doc: element-type requires at least one type");
268                    Self::Unknown(format_annotation(name, value))
269                }
270            },
271            "type" => match value {
272                Some(v) => Self::Type(v.to_owned()),
273                None => {
274                    tracing::warn!("doc: type requires a type name");
275                    Self::Unknown(format_annotation(name, value))
276                }
277            },
278            "attributes" => Self::Attributes(parse_attributes(value)),
279            _ => {
280                tracing::warn!(
281                    "doc: unknown return annotation: ({})",
282                    format_annotation(name, value)
283                );
284                Self::Unknown(format_annotation(name, value))
285            }
286        }
287    }
288
289    pub fn transfer(&self) -> Option<&TransferKind> {
290        if let Self::Transfer(k) = self {
291            Some(k)
292        } else {
293            None
294        }
295    }
296}
297
298/// Annotations valid on functions/methods.
299#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
300#[serde(rename_all = "kebab-case")]
301pub enum FunctionAnnotation {
302    Skip,
303    Constructor,
304    Method,
305    Virtual(String),
306    SetProperty(String),
307    GetProperty(String),
308    RenameTo(String),
309    SyncFunc(String),
310    AsyncFunc(String),
311    FinishFunc(String),
312}
313
314/// Annotations valid on type declarations (structs, boxed, fundamental).
315#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
316#[serde(rename_all = "kebab-case")]
317pub enum TypeAnnotation {
318    Skip,
319    Foreign,
320    RenameTo(String),
321    RefFunc(String),
322    UnrefFunc(String),
323    CopyFunc(String),
324    FreeFunc(String),
325    GetValueFunc(String),
326    SetValueFunc(String),
327}
328
329/// Annotations valid on properties.
330#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
331#[serde(rename_all = "kebab-case")]
332pub enum PropertyAnnotation {
333    Getter(String),
334    Setter(String),
335    DefaultValue(String),
336}
337
338/// Annotations valid on signals.
339#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
340#[serde(rename_all = "kebab-case")]
341pub enum SignalAnnotation {
342    Emitter(String),
343}
344
345/// Annotations valid on enum/flag values.
346#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
347#[serde(rename_all = "kebab-case")]
348pub enum EnumValueAnnotation {
349    Value(String),
350}
351
352fn parse_transfer(value: Option<&str>) -> Result<TransferKind, String> {
353    match value {
354        Some("none") => Ok(TransferKind::None),
355        Some("full") => Ok(TransferKind::Full),
356        Some("container") => Ok(TransferKind::Container),
357        Some("floating") => Ok(TransferKind::Floating),
358        _ => Err(format!("unknown transfer kind: {:?}", value)),
359    }
360}
361
362fn parse_scope(value: Option<&str>) -> Result<ScopeKind, String> {
363    match value {
364        Some("call") => Ok(ScopeKind::Call),
365        Some("async") => Ok(ScopeKind::Async),
366        Some("notified") => Ok(ScopeKind::Notified),
367        Some("forever") => Ok(ScopeKind::Forever),
368        _ => Err(format!("unknown scope kind: {:?}", value)),
369    }
370}
371
372fn parse_array(value: &str) -> ArrayAnnotation {
373    let mut length = None;
374    let mut fixed_size = None;
375    let mut zero_terminated = None;
376
377    for part in value.split_whitespace() {
378        if let Some(v) = part.strip_prefix("length=") {
379            length = Some(v.to_owned());
380        } else if let Some(v) = part.strip_prefix("fixed-size=") {
381            fixed_size = v.parse().ok();
382        } else if let Some(v) = part.strip_prefix("zero-terminated=") {
383            zero_terminated = match v {
384                "1" => Some(true),
385                "0" => Some(false),
386                _ => None,
387            };
388        }
389    }
390
391    ArrayAnnotation {
392        length,
393        fixed_size,
394        zero_terminated,
395    }
396}
397
398fn parse_attributes(value: Option<&str>) -> Vec<(String, String)> {
399    value
400        .unwrap_or("")
401        .split_whitespace()
402        .filter_map(|kv| {
403            let (k, v) = kv.split_once('=')?;
404            Some((k.to_owned(), v.to_owned()))
405        })
406        .collect()
407}
408
409fn format_annotation(name: &str, value: Option<&str>) -> String {
410    match value {
411        Some(v) => format!("{name} {v}"),
412        None => name.to_owned(),
413    }
414}
415
416fn parse_value_annotation<A>(
417    name: &str,
418    value: Option<&str>,
419    label: &str,
420    f: fn(String) -> A,
421) -> Option<A> {
422    match value {
423        Some(v) => Some(f(v.to_owned())),
424        None => {
425            tracing::warn!("doc: ({name}) requires {label}");
426            None
427        }
428    }
429}
430
431fn parse_function_annotation(name: &str, value: Option<&str>) -> Option<FunctionAnnotation> {
432    match name {
433        "skip" => Some(FunctionAnnotation::Skip),
434        "constructor" => Some(FunctionAnnotation::Constructor),
435        "method" => Some(FunctionAnnotation::Method),
436        "virtual" => {
437            parse_value_annotation(name, value, "a slot name", FunctionAnnotation::Virtual)
438        }
439        "set-property" => parse_value_annotation(
440            name,
441            value,
442            "a property name",
443            FunctionAnnotation::SetProperty,
444        ),
445        "get-property" => parse_value_annotation(
446            name,
447            value,
448            "a property name",
449            FunctionAnnotation::GetProperty,
450        ),
451        "rename-to" => {
452            parse_value_annotation(name, value, "a symbol name", FunctionAnnotation::RenameTo)
453        }
454        "sync-func" => {
455            parse_value_annotation(name, value, "a function name", FunctionAnnotation::SyncFunc)
456        }
457        "async-func" => parse_value_annotation(
458            name,
459            value,
460            "a function name",
461            FunctionAnnotation::AsyncFunc,
462        ),
463        "finish-func" => parse_value_annotation(
464            name,
465            value,
466            "a function name",
467            FunctionAnnotation::FinishFunc,
468        ),
469        _ => None,
470    }
471}
472
473fn parse_type_annotation(name: &str, value: Option<&str>) -> Option<TypeAnnotation> {
474    match name {
475        "skip" => Some(TypeAnnotation::Skip),
476        "foreign" => Some(TypeAnnotation::Foreign),
477        "rename-to" => {
478            parse_value_annotation(name, value, "a symbol name", TypeAnnotation::RenameTo)
479        }
480        "ref-func" => {
481            parse_value_annotation(name, value, "a function name", TypeAnnotation::RefFunc)
482        }
483        "unref-func" => {
484            parse_value_annotation(name, value, "a function name", TypeAnnotation::UnrefFunc)
485        }
486        "copy-func" => {
487            parse_value_annotation(name, value, "a function name", TypeAnnotation::CopyFunc)
488        }
489        "free-func" => {
490            parse_value_annotation(name, value, "a function name", TypeAnnotation::FreeFunc)
491        }
492        "get-value-func" => {
493            parse_value_annotation(name, value, "a function name", TypeAnnotation::GetValueFunc)
494        }
495        "set-value-func" => {
496            parse_value_annotation(name, value, "a function name", TypeAnnotation::SetValueFunc)
497        }
498        _ => None,
499    }
500}
501
502fn parse_property_annotation(name: &str, value: Option<&str>) -> Option<PropertyAnnotation> {
503    match name {
504        "getter" => {
505            parse_value_annotation(name, value, "a symbol name", PropertyAnnotation::Getter)
506        }
507        "setter" => {
508            parse_value_annotation(name, value, "a symbol name", PropertyAnnotation::Setter)
509        }
510        "default-value" => {
511            parse_value_annotation(name, value, "a value", PropertyAnnotation::DefaultValue)
512        }
513        _ => None,
514    }
515}
516
517fn parse_signal_annotation(name: &str, value: Option<&str>) -> Option<SignalAnnotation> {
518    match name {
519        "emitter" => {
520            parse_value_annotation(name, value, "a method name", SignalAnnotation::Emitter)
521        }
522        _ => None,
523    }
524}
525
526fn parse_enum_value_annotation(name: &str, value: Option<&str>) -> Option<EnumValueAnnotation> {
527    match name {
528        "value" => parse_value_annotation(name, value, "a value", EnumValueAnnotation::Value),
529        _ => None,
530    }
531}
532
533#[derive(Debug, Clone, Serialize)]
534pub struct DocParam {
535    pub name: String,
536    #[serde(skip_serializing_if = "Vec::is_empty")]
537    pub annotations: Vec<ParamAnnotation>,
538    #[serde(skip_serializing_if = "String::is_empty")]
539    pub description: String,
540}
541
542#[derive(Debug, Clone, Serialize)]
543pub struct DocReturns {
544    #[serde(skip_serializing_if = "Vec::is_empty")]
545    pub annotations: Vec<ReturnAnnotation>,
546    #[serde(skip_serializing_if = "String::is_empty")]
547    pub description: String,
548}
549
550struct RawDoc<A> {
551    symbol: Option<String>,
552    annotations: Vec<A>,
553    params: Vec<DocParam>,
554    returns: Option<DocReturns>,
555    description: Vec<String>,
556    since: Option<Version>,
557    deprecated: Option<(Version, Option<String>)>,
558}
559
560impl<A> RawDoc<A> {
561    fn from_node(
562        node: Node<'_>,
563        source: &[u8],
564        parse_annotation: fn(&str, Option<&str>) -> Option<A>,
565    ) -> Option<Self> {
566        // Try prev sibling first (normal case: comment sits before the node)
567        if let Some(prev) = node.prev_named_sibling()
568            && prev.kind() == "comment"
569            && let Ok(text) = std::str::from_utf8(&source[prev.byte_range()])
570            && text.starts_with("/**")
571        {
572            return Self::from_text(text, parse_annotation);
573        }
574
575        // When a macro_modifier precedes the doc comment (e.g.
576        // GSK_DEFINE_RENDER_NODE_TYPE(...) before the function), tree-sitter
577        // makes the comment a child of the function_definition instead of a
578        // preceding sibling. Scan children for a doc comment.
579        let mut cursor = node.walk();
580        for child in node.children(&mut cursor) {
581            if child.kind() == "comment"
582                && let Ok(text) = std::str::from_utf8(&source[child.byte_range()])
583                && text.starts_with("/**")
584            {
585                return Self::from_text(text, parse_annotation);
586            }
587        }
588
589        None
590    }
591
592    fn from_comment(
593        comment: &Comment,
594        parse_annotation: fn(&str, Option<&str>) -> Option<A>,
595    ) -> Option<Self> {
596        if !comment.is_gtk_doc() {
597            return None;
598        }
599        Self::from_text(&comment.text, parse_annotation)
600    }
601
602    fn from_text(
603        text: &str,
604        parse_annotation: fn(&str, Option<&str>) -> Option<A>,
605    ) -> Option<Self> {
606        let text = text.strip_prefix("/**")?.strip_suffix("*/")?.trim();
607
608        let mut symbol = None;
609        let mut annotations = Vec::new();
610        let mut params = Vec::new();
611        let mut returns = None;
612        let mut description = Vec::new();
613        let mut since = None;
614        let mut deprecated = None;
615        let mut past_symbol = false;
616        let mut in_param = false;
617
618        for raw_line in text.lines() {
619            let line = raw_line.trim().strip_prefix('*').unwrap_or(raw_line.trim());
620            let line = line.strip_prefix(' ').unwrap_or(line);
621
622            if line.is_empty() {
623                if in_param {
624                    in_param = false;
625                }
626                continue;
627            }
628
629            if let Some(rest) = line.strip_prefix('@')
630                && let Some((name, after_colon)) = rest.split_once(':')
631            {
632                let (anns, desc) =
633                    parse_annotations_and_desc(after_colon.trim(), ParamAnnotation::parse);
634                params.push(DocParam {
635                    name: name.trim().to_owned(),
636                    annotations: anns,
637                    description: desc,
638                });
639                in_param = true;
640            } else if in_param && line.starts_with(' ') {
641                // Continuation of previous param description
642                if let Some(last) = params.last_mut() {
643                    if !last.description.is_empty() {
644                        last.description.push(' ');
645                    }
646                    last.description.push_str(line.trim());
647                }
648            } else if let Some(after) = line.strip_prefix("Returns:") {
649                in_param = false;
650                let (anns, desc) =
651                    parse_annotations_and_desc(after.trim(), ReturnAnnotation::parse);
652                returns = Some(DocReturns {
653                    annotations: anns,
654                    description: desc,
655                });
656            } else if let Some(v) = line.strip_prefix("Since:") {
657                in_param = false;
658                since = v.trim().parse().ok();
659            } else if let Some(v) = line.strip_prefix("Deprecated:") {
660                in_param = false;
661                let v = v.trim();
662                let ver_end = v
663                    .find(|c: char| !(c.is_ascii_digit() || c == '.'))
664                    .unwrap_or(v.len());
665                let ver_str = v[..ver_end].trim_end_matches('.');
666                if let Ok(version) = ver_str.parse::<Version>() {
667                    let rest = v[ver_end..].trim().trim_start_matches(':').trim();
668                    let message = if rest.is_empty() {
669                        None
670                    } else {
671                        Some(rest.to_owned())
672                    };
673                    deprecated = Some((version, message));
674                }
675            } else if !past_symbol {
676                in_param = false;
677                let symbol_end = line
678                    .find(|c: char| !(c.is_alphanumeric() || c == '_' || c == ':' || c == '-'))
679                    .unwrap_or(line.len());
680                let candidate = &line[..symbol_end];
681                let rest = line[symbol_end..].trim();
682
683                let sym = candidate.trim_end_matches(':');
684                if !sym.is_empty()
685                    && candidate.ends_with(':')
686                    && sym
687                        .chars()
688                        .all(|c| c.is_alphanumeric() || c == '_' || c == ':' || c == '-')
689                {
690                    symbol = Some(sym.to_owned());
691                    past_symbol = true;
692                    if !rest.is_empty() {
693                        annotations = parse_symbol_annotations(rest, parse_annotation);
694                    }
695                } else {
696                    past_symbol = true;
697                    description.push(line.to_owned());
698                }
699            } else {
700                in_param = false;
701                description.push(line.to_owned());
702            }
703        }
704
705        Some(Self {
706            symbol,
707            annotations,
708            params,
709            returns,
710            description,
711            since,
712            deprecated,
713        })
714    }
715}
716
717#[derive(Debug, Clone, Serialize)]
718pub struct FunctionDoc {
719    #[serde(skip_serializing_if = "Option::is_none")]
720    pub symbol: Option<String>,
721    #[serde(skip_serializing_if = "Vec::is_empty")]
722    pub annotations: Vec<FunctionAnnotation>,
723    #[serde(skip_serializing_if = "Vec::is_empty")]
724    pub params: Vec<DocParam>,
725    #[serde(skip_serializing_if = "Option::is_none")]
726    pub returns: Option<DocReturns>,
727    #[serde(skip_serializing_if = "Vec::is_empty")]
728    pub description: Vec<String>,
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub since: Option<Version>,
731    #[serde(skip_serializing_if = "Option::is_none")]
732    pub deprecated: Option<(Version, Option<String>)>,
733}
734
735impl FunctionDoc {
736    pub fn from_node(node: Node<'_>, source: &[u8]) -> Option<Self> {
737        RawDoc::from_node(node, source, parse_function_annotation).map(Self::from_raw)
738    }
739
740    pub fn from_node_for(node: Node<'_>, source: &[u8], expected_name: &str) -> Option<Self> {
741        let doc = Self::from_node(node, source)?;
742        match &doc.symbol {
743            Some(sym) if sym != expected_name => None,
744            _ => Some(doc),
745        }
746    }
747
748    fn from_raw(raw: RawDoc<FunctionAnnotation>) -> Self {
749        Self {
750            symbol: raw.symbol,
751            annotations: raw.annotations,
752            params: raw.params,
753            returns: raw.returns,
754            description: raw.description,
755            since: raw.since,
756            deprecated: raw.deprecated,
757        }
758    }
759
760    pub fn param(&self, name: &str) -> Option<&DocParam> {
761        self.params.iter().find(|p| p.name == name)
762    }
763
764    pub fn param_has_annotation(&self, param: &str, annotation: &ParamAnnotation) -> bool {
765        self.param(param)
766            .is_some_and(|p| p.annotations.contains(annotation))
767    }
768
769    pub fn return_transfer(&self) -> Option<&TransferKind> {
770        self.returns
771            .as_ref()?
772            .annotations
773            .iter()
774            .find_map(|a| a.transfer())
775    }
776}
777
778#[derive(Debug, Clone, Serialize)]
779pub struct TypeDoc {
780    #[serde(skip_serializing_if = "Option::is_none")]
781    pub symbol: Option<String>,
782    #[serde(skip_serializing_if = "Vec::is_empty")]
783    pub annotations: Vec<TypeAnnotation>,
784    #[serde(skip_serializing_if = "Vec::is_empty")]
785    pub description: Vec<String>,
786    #[serde(skip_serializing_if = "Option::is_none")]
787    pub since: Option<Version>,
788    #[serde(skip_serializing_if = "Option::is_none")]
789    pub deprecated: Option<(Version, Option<String>)>,
790}
791
792impl TypeDoc {
793    pub fn from_node(node: Node<'_>, source: &[u8]) -> Option<Self> {
794        RawDoc::from_node(node, source, parse_type_annotation).map(Self::from_raw)
795    }
796
797    pub fn from_node_for(node: Node<'_>, source: &[u8], expected_name: &str) -> Option<Self> {
798        let doc = Self::from_node(node, source)?;
799        match &doc.symbol {
800            Some(sym) => {
801                let bare_sym = sym.trim_start_matches('_');
802                let bare_name = expected_name.trim_start_matches('_');
803                if bare_sym == bare_name {
804                    Some(doc)
805                } else {
806                    None
807                }
808            }
809            None => Some(doc),
810        }
811    }
812
813    pub fn from_comment(comment: &Comment) -> Option<Self> {
814        RawDoc::from_comment(comment, parse_type_annotation).map(Self::from_raw)
815    }
816
817    fn from_raw(raw: RawDoc<TypeAnnotation>) -> Self {
818        Self {
819            symbol: raw.symbol,
820            annotations: raw.annotations,
821            description: raw.description,
822            since: raw.since,
823            deprecated: raw.deprecated,
824        }
825    }
826}
827
828#[derive(Debug, Clone, Serialize)]
829pub struct PropertyDoc {
830    #[serde(skip_serializing_if = "Option::is_none")]
831    pub symbol: Option<String>,
832    #[serde(skip_serializing_if = "Vec::is_empty")]
833    pub annotations: Vec<PropertyAnnotation>,
834    #[serde(skip_serializing_if = "Vec::is_empty")]
835    pub description: Vec<String>,
836    #[serde(skip_serializing_if = "Option::is_none")]
837    pub since: Option<Version>,
838    #[serde(skip_serializing_if = "Option::is_none")]
839    pub deprecated: Option<(Version, Option<String>)>,
840}
841
842impl PropertyDoc {
843    pub fn from_comment(comment: &Comment) -> Option<Self> {
844        RawDoc::from_comment(comment, parse_property_annotation).map(Self::from_raw)
845    }
846
847    /// Attach only if the doc symbol matches `TypeName:property-name`.
848    pub fn from_comment_for(
849        comment: &Comment,
850        type_name: &str,
851        property_name: &str,
852    ) -> Option<Self> {
853        let doc = Self::from_comment(comment)?;
854        let expected = format!("{type_name}:{property_name}");
855        match &doc.symbol {
856            Some(sym) if sym != &expected => None,
857            _ => Some(doc),
858        }
859    }
860
861    fn from_raw(raw: RawDoc<PropertyAnnotation>) -> Self {
862        Self {
863            symbol: raw.symbol,
864            annotations: raw.annotations,
865            description: raw.description,
866            since: raw.since,
867            deprecated: raw.deprecated,
868        }
869    }
870}
871
872#[derive(Debug, Clone, Serialize)]
873pub struct SignalDoc {
874    #[serde(skip_serializing_if = "Option::is_none")]
875    pub symbol: Option<String>,
876    #[serde(skip_serializing_if = "Vec::is_empty")]
877    pub annotations: Vec<SignalAnnotation>,
878    #[serde(skip_serializing_if = "Vec::is_empty")]
879    pub params: Vec<DocParam>,
880    #[serde(skip_serializing_if = "Option::is_none")]
881    pub returns: Option<DocReturns>,
882    #[serde(skip_serializing_if = "Vec::is_empty")]
883    pub description: Vec<String>,
884    #[serde(skip_serializing_if = "Option::is_none")]
885    pub since: Option<Version>,
886    #[serde(skip_serializing_if = "Option::is_none")]
887    pub deprecated: Option<(Version, Option<String>)>,
888}
889
890impl SignalDoc {
891    pub fn from_comment(comment: &Comment) -> Option<Self> {
892        RawDoc::from_comment(comment, parse_signal_annotation).map(Self::from_raw)
893    }
894
895    /// Attach only if the doc symbol matches `TypeName::signal-name`.
896    pub fn from_comment_for(comment: &Comment, type_name: &str, signal_name: &str) -> Option<Self> {
897        let doc = Self::from_comment(comment)?;
898        let expected = format!("{type_name}::{signal_name}");
899        match &doc.symbol {
900            Some(sym) if sym != &expected => None,
901            _ => Some(doc),
902        }
903    }
904
905    fn from_raw(raw: RawDoc<SignalAnnotation>) -> Self {
906        Self {
907            symbol: raw.symbol,
908            annotations: raw.annotations,
909            params: raw.params,
910            returns: raw.returns,
911            description: raw.description,
912            since: raw.since,
913            deprecated: raw.deprecated,
914        }
915    }
916}
917
918#[derive(Debug, Clone, Serialize)]
919pub struct EnumValueDoc {
920    #[serde(skip_serializing_if = "Option::is_none")]
921    pub symbol: Option<String>,
922    #[serde(skip_serializing_if = "Vec::is_empty")]
923    pub annotations: Vec<EnumValueAnnotation>,
924    #[serde(skip_serializing_if = "Vec::is_empty")]
925    pub description: Vec<String>,
926    #[serde(skip_serializing_if = "Option::is_none")]
927    pub since: Option<Version>,
928    #[serde(skip_serializing_if = "Option::is_none")]
929    pub deprecated: Option<(Version, Option<String>)>,
930}
931
932impl EnumValueDoc {
933    pub fn from_comment(comment: &Comment) -> Option<Self> {
934        RawDoc::from_comment(comment, parse_enum_value_annotation).map(Self::from_raw)
935    }
936
937    /// Extract inline `@VALUE: description` entries from a parent type doc
938    /// comment (e.g. `/** GtkAlign: @GTK_ALIGN_FILL: ... */`).
939    pub fn extract_inline_from_comment(comment: &Comment) -> Vec<(String, Self)> {
940        let Some(raw) = RawDoc::from_comment(comment, parse_type_annotation) else {
941            return Vec::new();
942        };
943        raw.params
944            .into_iter()
945            .map(|p| {
946                let doc = Self {
947                    symbol: Some(p.name.clone()),
948                    annotations: Vec::new(),
949                    description: if p.description.is_empty() {
950                        Vec::new()
951                    } else {
952                        vec![p.description]
953                    },
954                    since: None,
955                    deprecated: None,
956                };
957                (p.name, doc)
958            })
959            .collect()
960    }
961
962    fn from_raw(raw: RawDoc<EnumValueAnnotation>) -> Self {
963        Self {
964            symbol: raw.symbol,
965            annotations: raw.annotations,
966            description: raw.description,
967            since: raw.since,
968            deprecated: raw.deprecated,
969        }
970    }
971}
972
973/// Parse `(annotation1) (annotation2): description text` using the
974/// provided parse function for the annotation type.
975fn is_annotation_name(name: &str) -> bool {
976    !name.is_empty()
977        && name
978            .chars()
979            .all(|c| c.is_ascii_lowercase() || c == '-' || c == ' ')
980}
981
982fn parse_annotations_and_desc<T>(
983    text: &str,
984    parse_fn: fn(&str, Option<&str>) -> T,
985) -> (Vec<T>, String) {
986    let mut annotations = Vec::new();
987    let mut rest = text;
988
989    // Annotations must appear consecutively at the start: (nullable)(transfer full)
990    while rest.starts_with('(') {
991        let Some(end) = rest.find(')') else {
992            break;
993        };
994        let inner = &rest[1..end];
995
996        let (name, value) = if let Some(rest_after) = inner.strip_prefix("not ") {
997            if let Some((_, v)) = rest_after.split_once(' ') {
998                (&inner[..inner.len() - v.len() - 1], Some(v))
999            } else {
1000                (inner, None)
1001            }
1002        } else {
1003            match inner.split_once(' ') {
1004                Some((n, v)) => (n, Some(v)),
1005                None => (inner, None),
1006            }
1007        };
1008
1009        if !is_annotation_name(name) {
1010            break;
1011        }
1012
1013        annotations.push(parse_fn(name, value));
1014
1015        rest = rest[end + 1..].trim_start();
1016    }
1017
1018    let desc = rest.strip_prefix(':').unwrap_or(rest).trim();
1019    (annotations, desc.to_owned())
1020}
1021
1022fn parse_symbol_annotations<A>(
1023    text: &str,
1024    parse_fn: fn(&str, Option<&str>) -> Option<A>,
1025) -> Vec<A> {
1026    let mut annotations = Vec::new();
1027    let mut rest = text;
1028
1029    while rest.starts_with('(') {
1030        let Some(end) = rest.find(')') else {
1031            break;
1032        };
1033        let inner = &rest[1..end];
1034
1035        let (name, value) = if let Some(rest_after) = inner.strip_prefix("not ") {
1036            if let Some((_, v)) = rest_after.split_once(' ') {
1037                (&inner[..inner.len() - v.len() - 1], Some(v))
1038            } else {
1039                (inner, None)
1040            }
1041        } else {
1042            match inner.split_once(' ') {
1043                Some((n, v)) => (n, Some(v)),
1044                None => (inner, None),
1045            }
1046        };
1047
1048        if !is_annotation_name(name) {
1049            break;
1050        }
1051
1052        if let Some(a) = parse_fn(name, value) {
1053            annotations.push(a);
1054        }
1055
1056        rest = rest[end + 1..].trim_start();
1057    }
1058
1059    annotations
1060}