Skip to main content

wit_parser/
metadata.rs

1//! Implementation of encoding/decoding package metadata (docs/stability) in a
2//! custom section.
3//!
4//! This module contains the particulars for how this custom section is encoded
5//! and decoded at this time. As of the time of this writing the component model
6//! binary format does not have any means of storing documentation and/or item
7//! stability inline with items themselves. These are important to preserve when
8//! round-tripping WIT through the WebAssembly binary format, however, so this
9//! module implements this with a custom section.
10//!
11//! The custom section, named `SECTION_NAME`, is stored within the component
12//! that encodes a WIT package. This section is itself JSON-encoded with a small
13//! version header to help forwards/backwards compatibility. The hope is that
14//! one day this custom section will be obsoleted by extensions to the binary
15//! format to store this information inline.
16
17use crate::{
18    Docs, Function, IndexMap, InterfaceId, PackageId, Resolve, Stability, TypeDefKind, TypeId,
19    WorldId, WorldItem, WorldKey,
20};
21use alloc::string::{String, ToString};
22#[cfg(feature = "serde")]
23use alloc::vec;
24#[cfg(feature = "serde")]
25use alloc::vec::Vec;
26use anyhow::{Result, bail};
27#[cfg(feature = "serde")]
28use serde_derive::{Deserialize, Serialize};
29
30type StringMap<V> = IndexMap<String, V>;
31
32/// Current supported format of the custom section.
33///
34/// This byte is a prefix byte intended to be a general version marker for the
35/// entire custom section. This is bumped when backwards-incompatible changes
36/// are made to prevent older implementations from loading newer versions.
37///
38/// The history of this is:
39///
40/// * [????/??/??] 0 - the original format added
41/// * [2024/04/19] 1 - extensions were added for item stability and
42///   additionally having world imports/exports have the same name.
43#[cfg(feature = "serde")]
44const PACKAGE_DOCS_SECTION_VERSION: u8 = 1;
45
46/// At this time the v1 format was just written. For compatibility with older
47/// tools we'll still try to emit the v0 format by default, if the input is
48/// compatible. This will be turned off in the future once enough published
49/// versions support the v1 format.
50const TRY_TO_EMIT_V0_BY_DEFAULT: bool = false;
51
52/// Represents serializable doc comments parsed from a WIT package.
53#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
54#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
55pub struct PackageMetadata {
56    #[cfg_attr(
57        feature = "serde",
58        serde(default, skip_serializing_if = "Option::is_none")
59    )]
60    docs: Option<String>,
61    #[cfg_attr(
62        feature = "serde",
63        serde(default, skip_serializing_if = "StringMap::is_empty")
64    )]
65    worlds: StringMap<WorldMetadata>,
66    #[cfg_attr(
67        feature = "serde",
68        serde(default, skip_serializing_if = "StringMap::is_empty")
69    )]
70    interfaces: StringMap<InterfaceMetadata>,
71}
72
73impl PackageMetadata {
74    pub const SECTION_NAME: &'static str = "package-docs";
75
76    /// Extract package docs for the given package.
77    pub fn extract(resolve: &Resolve, package: PackageId) -> Self {
78        let package = &resolve.packages[package];
79
80        let worlds = package
81            .worlds
82            .iter()
83            .map(|(name, id)| (name.to_string(), WorldMetadata::extract(resolve, *id)))
84            .filter(|(_, item)| !item.is_empty())
85            .collect();
86        let interfaces = package
87            .interfaces
88            .iter()
89            .map(|(name, id)| (name.to_string(), InterfaceMetadata::extract(resolve, *id)))
90            .filter(|(_, item)| !item.is_empty())
91            .collect();
92
93        Self {
94            docs: package.docs.contents.as_deref().map(Into::into),
95            worlds,
96            interfaces,
97        }
98    }
99
100    /// Inject package docs for the given package.
101    ///
102    /// This will override any existing docs in the [`Resolve`].
103    pub fn inject(&self, resolve: &mut Resolve, package: PackageId) -> Result<()> {
104        for (name, docs) in &self.worlds {
105            let Some(&id) = resolve.packages[package].worlds.get(name) else {
106                bail!("missing world {name:?}");
107            };
108            docs.inject(resolve, id)?;
109        }
110        for (name, docs) in &self.interfaces {
111            let Some(&id) = resolve.packages[package].interfaces.get(name) else {
112                bail!("missing interface {name:?}");
113            };
114            docs.inject(resolve, id)?;
115        }
116        if let Some(docs) = &self.docs {
117            resolve.packages[package].docs.contents = Some(docs.to_string());
118        }
119        Ok(())
120    }
121
122    /// Encode package docs as a package-docs custom section.
123    #[cfg(feature = "serde")]
124    pub fn encode(&self) -> Result<Vec<u8>> {
125        // Version byte, followed by JSON encoding of docs.
126        //
127        // Note that if this document is compatible with the v0 format then
128        // that's preferred to keep older tools working at this time.
129        // Eventually this branch will be removed and v1 will unconditionally
130        // be used.
131        let mut data = vec![
132            if TRY_TO_EMIT_V0_BY_DEFAULT && self.is_compatible_with_v0() {
133                0
134            } else {
135                PACKAGE_DOCS_SECTION_VERSION
136            },
137        ];
138        serde_json::to_writer(&mut data, self)?;
139        Ok(data)
140    }
141
142    /// Decode package docs from package-docs custom section content.
143    #[cfg(feature = "serde")]
144    pub fn decode(data: &[u8]) -> Result<Self> {
145        match data.first().copied() {
146            // Our serde structures transparently support v0 and the current
147            // version, so allow either here.
148            Some(0) | Some(PACKAGE_DOCS_SECTION_VERSION) => {}
149            version => {
150                bail!(
151                    "expected package-docs version {PACKAGE_DOCS_SECTION_VERSION}, got {version:?}"
152                );
153            }
154        }
155        Ok(serde_json::from_slice(&data[1..])?)
156    }
157
158    #[cfg(feature = "serde")]
159    fn is_compatible_with_v0(&self) -> bool {
160        self.worlds.iter().all(|(_, w)| w.is_compatible_with_v0())
161            && self
162                .interfaces
163                .iter()
164                .all(|(_, w)| w.is_compatible_with_v0())
165    }
166}
167
168#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
169#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
170struct WorldMetadata {
171    #[cfg_attr(
172        feature = "serde",
173        serde(default, skip_serializing_if = "Option::is_none")
174    )]
175    docs: Option<String>,
176    #[cfg_attr(
177        feature = "serde",
178        serde(default, skip_serializing_if = "Stability::is_unknown")
179    )]
180    stability: Stability,
181
182    /// Metadata for named interface, e.g.:
183    ///
184    /// ```wit
185    /// world foo {
186    ///     import x: interface {}
187    /// }
188    /// ```
189    ///
190    /// In the v0 format this was called "interfaces", hence the
191    /// `serde(rename)`. When support was originally added here imports/exports
192    /// could not overlap in their name, but now they can. This map has thus
193    /// been repurposed as:
194    ///
195    /// * If an interface is imported, it goes here.
196    /// * If an interface is exported, and no interface was imported with the
197    ///   same name, it goes here.
198    ///
199    /// Otherwise exports go inside the `interface_exports` map.
200    ///
201    /// In the future when v0 support is dropped this should become only
202    /// imports, not either imports-or-exports.
203    #[cfg_attr(
204        feature = "serde",
205        serde(
206            default,
207            rename = "interfaces",
208            skip_serializing_if = "StringMap::is_empty"
209        )
210    )]
211    interface_imports_or_exports: StringMap<InterfaceMetadata>,
212
213    /// All types in this interface.
214    ///
215    /// Note that at this time types are only imported, never exported.
216    #[cfg_attr(
217        feature = "serde",
218        serde(default, skip_serializing_if = "StringMap::is_empty")
219    )]
220    types: StringMap<TypeMetadata>,
221
222    /// Same as `interface_imports_or_exports`, but for functions.
223    #[cfg_attr(
224        feature = "serde",
225        serde(default, rename = "funcs", skip_serializing_if = "StringMap::is_empty")
226    )]
227    func_imports_or_exports: StringMap<FunctionMetadata>,
228
229    /// The "export half" of `interface_imports_or_exports`.
230    #[cfg_attr(
231        feature = "serde",
232        serde(default, skip_serializing_if = "StringMap::is_empty")
233    )]
234    interface_exports: StringMap<InterfaceMetadata>,
235
236    /// The "export half" of `func_imports_or_exports`.
237    #[cfg_attr(
238        feature = "serde",
239        serde(default, skip_serializing_if = "StringMap::is_empty")
240    )]
241    func_exports: StringMap<FunctionMetadata>,
242
243    /// Stability annotations for interface imports that aren't inline, for
244    /// example:
245    ///
246    /// ```wit
247    /// world foo {
248    ///     @since(version = 1.0.0)
249    ///     import an-interface;
250    /// }
251    /// ```
252    #[cfg_attr(
253        feature = "serde",
254        serde(default, skip_serializing_if = "StringMap::is_empty")
255    )]
256    interface_import_stability: StringMap<Stability>,
257
258    /// Same as `interface_import_stability`, but for exports.
259    #[cfg_attr(
260        feature = "serde",
261        serde(default, skip_serializing_if = "StringMap::is_empty")
262    )]
263    interface_export_stability: StringMap<Stability>,
264
265    /// Docs attached to interface imports that aren't inline, mirroring
266    /// `interface_import_stability`, for example:
267    ///
268    /// ```wit
269    /// world foo {
270    ///     /// These docs.
271    ///     import an-interface;
272    /// }
273    /// ```
274    #[cfg_attr(
275        feature = "serde",
276        serde(default, skip_serializing_if = "StringMap::is_empty")
277    )]
278    interface_import_docs: StringMap<String>,
279
280    /// Same as `interface_import_docs`, but for exports.
281    #[cfg_attr(
282        feature = "serde",
283        serde(default, skip_serializing_if = "StringMap::is_empty")
284    )]
285    interface_export_docs: StringMap<String>,
286}
287
288impl WorldMetadata {
289    fn extract(resolve: &Resolve, id: WorldId) -> Self {
290        let world = &resolve.worlds[id];
291
292        let mut interface_imports_or_exports = StringMap::default();
293        let mut types = StringMap::default();
294        let mut func_imports_or_exports = StringMap::default();
295        let mut interface_exports = StringMap::default();
296        let mut func_exports = StringMap::default();
297        let mut interface_import_stability = StringMap::default();
298        let mut interface_export_stability = StringMap::default();
299        let mut interface_import_docs = StringMap::default();
300        let mut interface_export_docs = StringMap::default();
301
302        // Records the stability and docs of a non-inline (by-reference)
303        // interface import/export, keyed by its world-key name, so that they
304        // can be restored by `inject`. Docs and stability are recorded
305        // independently since either may be present without the other.
306        let mut record_interface_metadata = |key: &WorldKey, item: &WorldItem, import: bool| {
307            let (stability, docs) = match item {
308                WorldItem::Interface {
309                    stability, docs, ..
310                } => (stability, docs),
311                _ => return,
312            };
313            let name = resolve.name_world_key(key);
314            if !stability.is_unknown() {
315                let map = if import {
316                    &mut interface_import_stability
317                } else {
318                    &mut interface_export_stability
319                };
320                map.insert(name.clone(), stability.clone());
321            }
322            if let Some(contents) = &docs.contents {
323                let map = if import {
324                    &mut interface_import_docs
325                } else {
326                    &mut interface_export_docs
327                };
328                map.insert(name, contents.clone());
329            }
330        };
331
332        for ((key, item), import) in world
333            .imports
334            .iter()
335            .map(|p| (p, true))
336            .chain(world.exports.iter().map(|p| (p, false)))
337        {
338            match key {
339                // For all named imports with kebab-names extract their
340                // docs/stability and insert it into one of our maps.
341                WorldKey::Name(name) => match item {
342                    WorldItem::Interface { id, .. } => {
343                        if resolve.interfaces[*id].name.is_some() {
344                            record_interface_metadata(key, item, import);
345                            continue;
346                        }
347                        let data = InterfaceMetadata::extract(resolve, *id);
348                        if data.is_empty() {
349                            continue;
350                        }
351                        let map = if import {
352                            &mut interface_imports_or_exports
353                        } else if !TRY_TO_EMIT_V0_BY_DEFAULT
354                            || interface_imports_or_exports.contains_key(name)
355                        {
356                            &mut interface_exports
357                        } else {
358                            &mut interface_imports_or_exports
359                        };
360                        let prev = map.insert(name.to_string(), data);
361                        assert!(prev.is_none());
362                    }
363                    WorldItem::Type { id, .. } => {
364                        let data = TypeMetadata::extract(resolve, *id);
365                        if !data.is_empty() {
366                            types.insert(name.to_string(), data);
367                        }
368                    }
369                    WorldItem::Function(f) => {
370                        let data = FunctionMetadata::extract(f);
371                        if data.is_empty() {
372                            continue;
373                        }
374                        let map = if import {
375                            &mut func_imports_or_exports
376                        } else if !TRY_TO_EMIT_V0_BY_DEFAULT
377                            || func_imports_or_exports.contains_key(name)
378                        {
379                            &mut func_exports
380                        } else {
381                            &mut func_imports_or_exports
382                        };
383                        let prev = map.insert(name.to_string(), data);
384                        assert!(prev.is_none());
385                    }
386                },
387
388                // For interface imports/exports extract the stability/docs and
389                // record them if necessary.
390                WorldKey::Interface(_) => {
391                    record_interface_metadata(key, item, import);
392                }
393            }
394        }
395
396        Self {
397            docs: world.docs.contents.clone(),
398            stability: world.stability.clone(),
399            interface_imports_or_exports,
400            types,
401            func_imports_or_exports,
402            interface_exports,
403            func_exports,
404            interface_import_stability,
405            interface_export_stability,
406            interface_import_docs,
407            interface_export_docs,
408        }
409    }
410
411    fn inject(&self, resolve: &mut Resolve, id: WorldId) -> Result<()> {
412        // Inject docs/stability for all kebab-named interfaces, both imports
413        // and exports.
414        for ((name, data), only_export) in self
415            .interface_imports_or_exports
416            .iter()
417            .map(|p| (p, false))
418            .chain(self.interface_exports.iter().map(|p| (p, true)))
419        {
420            let key = WorldKey::Name(name.to_string());
421            let world = &mut resolve.worlds[id];
422
423            let item = if only_export {
424                world.exports.get_mut(&key)
425            } else {
426                match world.imports.get_mut(&key) {
427                    Some(item) => Some(item),
428                    None => world.exports.get_mut(&key),
429                }
430            };
431            let Some(WorldItem::Interface { id, stability, .. }) = item else {
432                bail!("missing interface {name:?}");
433            };
434            *stability = data.stability.clone();
435            let id = *id;
436            data.inject(resolve, id)?;
437        }
438
439        // Process all types, which are always imported, for this world.
440        for (name, data) in &self.types {
441            let key = WorldKey::Name(name.to_string());
442            let Some(WorldItem::Type { id, .. }) = resolve.worlds[id].imports.get(&key) else {
443                bail!("missing type {name:?}");
444            };
445            data.inject(resolve, *id)?;
446        }
447
448        // Build a map of `name_world_key` for interface imports/exports to the
449        // actual key. This map is then consluted in the next loop.
450        let world = &resolve.worlds[id];
451        let stabilities = world
452            .imports
453            .iter()
454            .map(|i| (i, true))
455            .chain(world.exports.iter().map(|i| (i, false)))
456            .filter_map(|((key, item), import)| match item {
457                WorldItem::Interface { .. } => {
458                    Some(((resolve.name_world_key(key), import), key.clone()))
459                }
460                _ => None,
461            })
462            .collect::<IndexMap<_, _>>();
463
464        let world = &mut resolve.worlds[id];
465
466        // Update the stability of an interface imports/exports that aren't
467        // kebab-named.
468        for ((name, stability), import) in self
469            .interface_import_stability
470            .iter()
471            .map(|p| (p, true))
472            .chain(self.interface_export_stability.iter().map(|p| (p, false)))
473        {
474            let key = match stabilities.get(&(name.clone(), import)) {
475                Some(key) => key.clone(),
476                None => bail!("missing interface `{name}`"),
477            };
478            let item = if import {
479                world.imports.get_mut(&key)
480            } else {
481                world.exports.get_mut(&key)
482            };
483            match item {
484                Some(WorldItem::Interface { stability: s, .. }) => *s = stability.clone(),
485                _ => bail!("item `{name}` wasn't an interface"),
486            }
487        }
488
489        // Update the docs of interface imports/exports that aren't kebab-named,
490        // reusing the same `stabilities` key map built above.
491        for ((name, docs), import) in self
492            .interface_import_docs
493            .iter()
494            .map(|p| (p, true))
495            .chain(self.interface_export_docs.iter().map(|p| (p, false)))
496        {
497            let key = match stabilities.get(&(name.clone(), import)) {
498                Some(key) => key.clone(),
499                None => bail!("missing interface `{name}`"),
500            };
501            let item = if import {
502                world.imports.get_mut(&key)
503            } else {
504                world.exports.get_mut(&key)
505            };
506            match item {
507                Some(WorldItem::Interface { docs: d, .. }) => {
508                    d.contents = Some(docs.clone());
509                }
510                _ => bail!("item `{name}` wasn't an interface"),
511            }
512        }
513
514        // Update the docs/stability of all functions imported/exported from
515        // this world.
516        for ((name, data), only_export) in self
517            .func_imports_or_exports
518            .iter()
519            .map(|p| (p, false))
520            .chain(self.func_exports.iter().map(|p| (p, true)))
521        {
522            let key = WorldKey::Name(name.to_string());
523            let item = if only_export {
524                world.exports.get_mut(&key)
525            } else {
526                match world.imports.get_mut(&key) {
527                    Some(item) => Some(item),
528                    None => world.exports.get_mut(&key),
529                }
530            };
531            match item {
532                Some(WorldItem::Function(f)) => data.inject(f)?,
533                _ => bail!("missing func {name:?}"),
534            }
535        }
536        if let Some(docs) = &self.docs {
537            world.docs.contents = Some(docs.to_string());
538        }
539        world.stability = self.stability.clone();
540        Ok(())
541    }
542
543    fn is_empty(&self) -> bool {
544        self.docs.is_none()
545            && self.interface_imports_or_exports.is_empty()
546            && self.types.is_empty()
547            && self.func_imports_or_exports.is_empty()
548            && self.stability.is_unknown()
549            && self.interface_exports.is_empty()
550            && self.func_exports.is_empty()
551            && self.interface_import_stability.is_empty()
552            && self.interface_export_stability.is_empty()
553            && self.interface_import_docs.is_empty()
554            && self.interface_export_docs.is_empty()
555    }
556
557    #[cfg(feature = "serde")]
558    fn is_compatible_with_v0(&self) -> bool {
559        self.stability.is_unknown()
560            && self
561                .interface_imports_or_exports
562                .iter()
563                .all(|(_, w)| w.is_compatible_with_v0())
564            && self
565                .func_imports_or_exports
566                .iter()
567                .all(|(_, w)| w.is_compatible_with_v0())
568            && self.types.iter().all(|(_, w)| w.is_compatible_with_v0())
569            // These maps weren't present in v0, so we're only compatible if
570            // they're empty.
571            && self.interface_exports.is_empty()
572            && self.func_exports.is_empty()
573            && self.interface_import_stability.is_empty()
574            && self.interface_export_stability.is_empty()
575            && self.interface_import_docs.is_empty()
576            && self.interface_export_docs.is_empty()
577    }
578}
579
580#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
581#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
582struct InterfaceMetadata {
583    #[cfg_attr(
584        feature = "serde",
585        serde(default, skip_serializing_if = "Option::is_none")
586    )]
587    docs: Option<String>,
588    #[cfg_attr(
589        feature = "serde",
590        serde(default, skip_serializing_if = "Stability::is_unknown")
591    )]
592    stability: Stability,
593    #[cfg_attr(
594        feature = "serde",
595        serde(default, skip_serializing_if = "StringMap::is_empty")
596    )]
597    funcs: StringMap<FunctionMetadata>,
598    #[cfg_attr(
599        feature = "serde",
600        serde(default, skip_serializing_if = "StringMap::is_empty")
601    )]
602    types: StringMap<TypeMetadata>,
603}
604
605impl InterfaceMetadata {
606    fn extract(resolve: &Resolve, id: InterfaceId) -> Self {
607        let interface = &resolve.interfaces[id];
608
609        let funcs = interface
610            .functions
611            .iter()
612            .map(|(name, func)| (name.to_string(), FunctionMetadata::extract(func)))
613            .filter(|(_, item)| !item.is_empty())
614            .collect();
615        let types = interface
616            .types
617            .iter()
618            .map(|(name, id)| (name.to_string(), TypeMetadata::extract(resolve, *id)))
619            .filter(|(_, item)| !item.is_empty())
620            .collect();
621
622        Self {
623            docs: interface.docs.contents.clone(),
624            stability: interface.stability.clone(),
625            funcs,
626            types,
627        }
628    }
629
630    fn inject(&self, resolve: &mut Resolve, id: InterfaceId) -> Result<()> {
631        for (name, data) in &self.types {
632            let Some(&id) = resolve.interfaces[id].types.get(name) else {
633                bail!("missing type {name:?}");
634            };
635            data.inject(resolve, id)?;
636        }
637        let interface = &mut resolve.interfaces[id];
638        for (name, data) in &self.funcs {
639            let Some(f) = interface.functions.get_mut(name) else {
640                bail!("missing func {name:?}");
641            };
642            data.inject(f)?;
643        }
644        if let Some(docs) = &self.docs {
645            interface.docs.contents = Some(docs.to_string());
646        }
647        interface.stability = self.stability.clone();
648        Ok(())
649    }
650
651    fn is_empty(&self) -> bool {
652        self.docs.is_none()
653            && self.funcs.is_empty()
654            && self.types.is_empty()
655            && self.stability.is_unknown()
656    }
657
658    #[cfg(feature = "serde")]
659    fn is_compatible_with_v0(&self) -> bool {
660        self.stability.is_unknown()
661            && self.funcs.iter().all(|(_, w)| w.is_compatible_with_v0())
662            && self.types.iter().all(|(_, w)| w.is_compatible_with_v0())
663    }
664}
665
666#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
667#[cfg_attr(feature = "serde", serde(untagged, deny_unknown_fields))]
668enum FunctionMetadata {
669    /// In the v0 format function metadata was only a string so this variant
670    /// is preserved for the v0 format. In the future this can be removed
671    /// entirely in favor of just the below struct variant.
672    ///
673    /// Note that this is an untagged enum so the name `JustDocs` is just for
674    /// rust.
675    JustDocs(Option<String>),
676
677    /// In the v1+ format we're tracking at least docs but also the stability
678    /// of functions.
679    DocsAndStability {
680        #[cfg_attr(
681            feature = "serde",
682            serde(default, skip_serializing_if = "Option::is_none")
683        )]
684        docs: Option<String>,
685        #[cfg_attr(
686            feature = "serde",
687            serde(default, skip_serializing_if = "Stability::is_unknown")
688        )]
689        stability: Stability,
690    },
691}
692
693impl FunctionMetadata {
694    fn extract(func: &Function) -> Self {
695        if TRY_TO_EMIT_V0_BY_DEFAULT && func.stability.is_unknown() {
696            FunctionMetadata::JustDocs(func.docs.contents.clone())
697        } else {
698            FunctionMetadata::DocsAndStability {
699                docs: func.docs.contents.clone(),
700                stability: func.stability.clone(),
701            }
702        }
703    }
704
705    fn inject(&self, func: &mut Function) -> Result<()> {
706        match self {
707            FunctionMetadata::JustDocs(docs) => {
708                func.docs.contents = docs.clone();
709            }
710            FunctionMetadata::DocsAndStability { docs, stability } => {
711                func.docs.contents = docs.clone();
712                func.stability = stability.clone();
713            }
714        }
715        Ok(())
716    }
717
718    fn is_empty(&self) -> bool {
719        match self {
720            FunctionMetadata::JustDocs(docs) => docs.is_none(),
721            FunctionMetadata::DocsAndStability { docs, stability } => {
722                docs.is_none() && stability.is_unknown()
723            }
724        }
725    }
726
727    #[cfg(feature = "serde")]
728    fn is_compatible_with_v0(&self) -> bool {
729        match self {
730            FunctionMetadata::JustDocs(_) => true,
731            FunctionMetadata::DocsAndStability { .. } => false,
732        }
733    }
734}
735
736#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
737#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
738struct TypeMetadata {
739    #[cfg_attr(
740        feature = "serde",
741        serde(default, skip_serializing_if = "Option::is_none")
742    )]
743    docs: Option<String>,
744    #[cfg_attr(
745        feature = "serde",
746        serde(default, skip_serializing_if = "Stability::is_unknown")
747    )]
748    stability: Stability,
749    // record fields, variant cases, etc.
750    #[cfg_attr(
751        feature = "serde",
752        serde(default, skip_serializing_if = "StringMap::is_empty")
753    )]
754    items: StringMap<String>,
755}
756
757impl TypeMetadata {
758    fn extract(resolve: &Resolve, id: TypeId) -> Self {
759        fn extract_items<T>(items: &[T], f: impl Fn(&T) -> (&String, &Docs)) -> StringMap<String> {
760            items
761                .iter()
762                .flat_map(|item| {
763                    let (name, docs) = f(item);
764                    Some((name.to_string(), docs.contents.clone()?))
765                })
766                .collect()
767        }
768        let ty = &resolve.types[id];
769        let items = match &ty.kind {
770            TypeDefKind::Record(record) => {
771                extract_items(&record.fields, |item| (&item.name, &item.docs))
772            }
773            TypeDefKind::Flags(flags) => {
774                extract_items(&flags.flags, |item| (&item.name, &item.docs))
775            }
776            TypeDefKind::Variant(variant) => {
777                extract_items(&variant.cases, |item| (&item.name, &item.docs))
778            }
779            TypeDefKind::Enum(enum_) => {
780                extract_items(&enum_.cases, |item| (&item.name, &item.docs))
781            }
782            // other types don't have inner items
783            _ => IndexMap::default(),
784        };
785
786        Self {
787            docs: ty.docs.contents.clone(),
788            stability: ty.stability.clone(),
789            items,
790        }
791    }
792
793    fn inject(&self, resolve: &mut Resolve, id: TypeId) -> Result<()> {
794        let ty = &mut resolve.types[id];
795        if !self.items.is_empty() {
796            match &mut ty.kind {
797                TypeDefKind::Record(record) => {
798                    self.inject_items(&mut record.fields, |item| (&item.name, &mut item.docs))?
799                }
800                TypeDefKind::Flags(flags) => {
801                    self.inject_items(&mut flags.flags, |item| (&item.name, &mut item.docs))?
802                }
803                TypeDefKind::Variant(variant) => {
804                    self.inject_items(&mut variant.cases, |item| (&item.name, &mut item.docs))?
805                }
806                TypeDefKind::Enum(enum_) => {
807                    self.inject_items(&mut enum_.cases, |item| (&item.name, &mut item.docs))?
808                }
809                _ => {
810                    bail!("got 'items' for unexpected type {ty:?}");
811                }
812            }
813        }
814        if let Some(docs) = &self.docs {
815            ty.docs.contents = Some(docs.to_string());
816        }
817        ty.stability = self.stability.clone();
818        Ok(())
819    }
820
821    fn inject_items<T: core::fmt::Debug>(
822        &self,
823        items: &mut [T],
824        f: impl Fn(&mut T) -> (&String, &mut Docs),
825    ) -> Result<()> {
826        let mut unused_docs = self.items.len();
827        for item in items.iter_mut() {
828            let (name, item_docs) = f(item);
829            if let Some(docs) = self.items.get(name.as_str()) {
830                item_docs.contents = Some(docs.to_string());
831                unused_docs -= 1;
832            }
833        }
834        if unused_docs > 0 {
835            bail!(
836                "not all 'items' match type items; {item_docs:?} vs {items:?}",
837                item_docs = self.items
838            );
839        }
840        Ok(())
841    }
842
843    fn is_empty(&self) -> bool {
844        self.docs.is_none() && self.items.is_empty() && self.stability.is_unknown()
845    }
846
847    #[cfg(feature = "serde")]
848    fn is_compatible_with_v0(&self) -> bool {
849        self.stability.is_unknown()
850    }
851}