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#[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#[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#[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#[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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
340#[serde(rename_all = "kebab-case")]
341pub enum SignalAnnotation {
342 Emitter(String),
343}
344
345#[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 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 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 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 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 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 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
973fn 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 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}