blob: 103e3d03af1eb3f1d777ce7b42821c679ef532a8 [file] [log] [blame] [edit]
//! Implementation of encoding/decoding package metadata (docs/stability) in a
//! custom section.
//!
//! This module contains the particulars for how this custom section is encoded
//! and decoded at this time. As of the time of this writing the component model
//! binary format does not have any means of storing documentation and/or item
//! stability inline with items themselves. These are important to preserve when
//! round-tripping WIT through the WebAssembly binary format, however, so this
//! module implements this with a custom section.
//!
//! The custom section, named `SECTION_NAME`, is stored within the component
//! that encodes a WIT package. This section is itself JSON-encoded with a small
//! version header to help forwards/backwards compatibility. The hope is that
//! one day this custom section will be obsoleted by extensions to the binary
//! format to store this information inline.
use crate::{
Docs, Function, InterfaceId, PackageId, Resolve, Stability, TypeDefKind, TypeId, WorldId,
WorldItem, WorldKey,
};
use anyhow::{bail, Result};
use indexmap::IndexMap;
#[cfg(feature = "serde")]
use serde_derive::{Deserialize, Serialize};
type StringMap<V> = IndexMap<String, V>;
/// Current supported format of the custom section.
///
/// This byte is a prefix byte intended to be a general version marker for the
/// entire custom section. This is bumped when backwards-incompatible changes
/// are made to prevent older implementations from loading newer versions.
///
/// The history of this is:
///
/// * [????/??/??] 0 - the original format added
/// * [2024/04/19] 1 - extensions were added for item stability and
/// additionally having world imports/exports have the same name.
#[cfg(feature = "serde")]
const PACKAGE_DOCS_SECTION_VERSION: u8 = 1;
/// At this time the v1 format was just written. For compatibility with older
/// tools we'll still try to emit the v0 format by default, if the input is
/// compatible. This will be turned off in the future once enough published
/// versions support the v1 format.
const TRY_TO_EMIT_V0_BY_DEFAULT: bool = true;
/// Represents serializable doc comments parsed from a WIT package.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub struct PackageMetadata {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
docs: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "StringMap::is_empty")
)]
worlds: StringMap<WorldMetadata>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "StringMap::is_empty")
)]
interfaces: StringMap<InterfaceMetadata>,
}
impl PackageMetadata {
pub const SECTION_NAME: &'static str = "package-docs";
/// Extract package docs for the given package.
pub fn extract(resolve: &Resolve, package: PackageId) -> Self {
let package = &resolve.packages[package];
let worlds = package
.worlds
.iter()
.map(|(name, id)| (name.to_string(), WorldMetadata::extract(resolve, *id)))
.filter(|(_, item)| !item.is_empty())
.collect();
let interfaces = package
.interfaces
.iter()
.map(|(name, id)| (name.to_string(), InterfaceMetadata::extract(resolve, *id)))
.filter(|(_, item)| !item.is_empty())
.collect();
Self {
docs: package.docs.contents.as_deref().map(Into::into),
worlds,
interfaces,
}
}
/// Inject package docs for the given package.
///
/// This will override any existing docs in the [`Resolve`].
pub fn inject(&self, resolve: &mut Resolve, package: PackageId) -> Result<()> {
for (name, docs) in &self.worlds {
let Some(&id) = resolve.packages[package].worlds.get(name) else {
bail!("missing world {name:?}");
};
docs.inject(resolve, id)?;
}
for (name, docs) in &self.interfaces {
let Some(&id) = resolve.packages[package].interfaces.get(name) else {
bail!("missing interface {name:?}");
};
docs.inject(resolve, id)?;
}
if let Some(docs) = &self.docs {
resolve.packages[package].docs.contents = Some(docs.to_string());
}
Ok(())
}
/// Encode package docs as a package-docs custom section.
#[cfg(feature = "serde")]
pub fn encode(&self) -> Result<Vec<u8>> {
// Version byte, followed by JSON encoding of docs.
//
// Note that if this document is compatible with the v0 format then
// that's preferred to keep older tools working at this time.
// Eventually this branch will be removed and v1 will unconditionally
// be used.
let mut data = vec![
if TRY_TO_EMIT_V0_BY_DEFAULT && self.is_compatible_with_v0() {
0
} else {
PACKAGE_DOCS_SECTION_VERSION
},
];
serde_json::to_writer(&mut data, self)?;
Ok(data)
}
/// Decode package docs from package-docs custom section content.
#[cfg(feature = "serde")]
pub fn decode(data: &[u8]) -> Result<Self> {
match data.first().copied() {
// Our serde structures transparently support v0 and the current
// version, so allow either here.
Some(0) | Some(PACKAGE_DOCS_SECTION_VERSION) => {}
version => {
bail!(
"expected package-docs version {PACKAGE_DOCS_SECTION_VERSION}, got {version:?}"
);
}
}
Ok(serde_json::from_slice(&data[1..])?)
}
#[cfg(feature = "serde")]
fn is_compatible_with_v0(&self) -> bool {
self.worlds.iter().all(|(_, w)| w.is_compatible_with_v0())
&& self
.interfaces
.iter()
.all(|(_, w)| w.is_compatible_with_v0())
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
struct WorldMetadata {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
docs: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Stability::is_unknown")
)]
stability: Stability,
/// Metadata for named interface, e.g.:
///
/// ```wit
/// world foo {
/// import x: interface {}
/// }
/// ```
///
/// In the v0 format this was called "interfaces", hence the
/// `serde(rename)`. When support was originally added here imports/exports
/// could not overlap in their name, but now they can. This map has thus
/// been repurposed as:
///
/// * If an interface is imported, it goes here.
/// * If an interface is exported, and no interface was imported with the
/// same name, it goes here.
///
/// Otherwise exports go inside the `interface_exports` map.
///
/// In the future when v0 support is dropped this should become only
/// imports, not either imports-or-exports.
#[cfg_attr(
feature = "serde",
serde(
default,
rename = "interfaces",
skip_serializing_if = "StringMap::is_empty"
)
)]
interface_imports_or_exports: StringMap<InterfaceMetadata>,
/// All types in this interface.
///
/// Note that at this time types are only imported, never exported.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "StringMap::is_empty")
)]
types: StringMap<TypeMetadata>,
/// Same as `interface_imports_or_exports`, but for functions.
#[cfg_attr(
feature = "serde",
serde(default, rename = "funcs", skip_serializing_if = "StringMap::is_empty")
)]
func_imports_or_exports: StringMap<FunctionMetadata>,
/// The "export half" of `interface_imports_or_exports`.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "StringMap::is_empty")
)]
interface_exports: StringMap<InterfaceMetadata>,
/// The "export half" of `func_imports_or_exports`.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "StringMap::is_empty")
)]
func_exports: StringMap<FunctionMetadata>,
/// Stability annotations for interface imports that aren't inline, for
/// example:
///
/// ```wit
/// world foo {
/// @since(version = 1.0.0)
/// import an-interface;
/// }
/// ```
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "StringMap::is_empty")
)]
interface_import_stability: StringMap<Stability>,
/// Same as `interface_import_stability`, but for exports.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "StringMap::is_empty")
)]
interface_export_stability: StringMap<Stability>,
}
impl WorldMetadata {
fn extract(resolve: &Resolve, id: WorldId) -> Self {
let world = &resolve.worlds[id];
let mut interface_imports_or_exports = StringMap::default();
let mut types = StringMap::default();
let mut func_imports_or_exports = StringMap::default();
let mut interface_exports = StringMap::default();
let mut func_exports = StringMap::default();
let mut interface_import_stability = StringMap::default();
let mut interface_export_stability = StringMap::default();
for ((key, item), import) in world
.imports
.iter()
.map(|p| (p, true))
.chain(world.exports.iter().map(|p| (p, false)))
{
match key {
// For all named imports with kebab-names extract their
// docs/stability and insert it into one of our maps.
WorldKey::Name(name) => match item {
WorldItem::Interface { id, .. } => {
let data = InterfaceMetadata::extract(resolve, *id);
if data.is_empty() {
continue;
}
let map = if import {
&mut interface_imports_or_exports
} else if !TRY_TO_EMIT_V0_BY_DEFAULT
|| interface_imports_or_exports.contains_key(name)
{
&mut interface_exports
} else {
&mut interface_imports_or_exports
};
let prev = map.insert(name.to_string(), data);
assert!(prev.is_none());
}
WorldItem::Type(id) => {
let data = TypeMetadata::extract(resolve, *id);
if !data.is_empty() {
types.insert(name.to_string(), data);
}
}
WorldItem::Function(f) => {
let data = FunctionMetadata::extract(f);
if data.is_empty() {
continue;
}
let map = if import {
&mut func_imports_or_exports
} else if !TRY_TO_EMIT_V0_BY_DEFAULT
|| func_imports_or_exports.contains_key(name)
{
&mut func_exports
} else {
&mut func_imports_or_exports
};
let prev = map.insert(name.to_string(), data);
assert!(prev.is_none());
}
},
// For interface imports/exports extract the stability and
// record it if necessary.
WorldKey::Interface(_) => {
let stability = match item {
WorldItem::Interface { stability, .. } => stability,
_ => continue,
};
if stability.is_unknown() {
continue;
}
let map = if import {
&mut interface_import_stability
} else {
&mut interface_export_stability
};
let name = resolve.name_world_key(key);
map.insert(name, stability.clone());
}
}
}
Self {
docs: world.docs.contents.clone(),
stability: world.stability.clone(),
interface_imports_or_exports,
types,
func_imports_or_exports,
interface_exports,
func_exports,
interface_import_stability,
interface_export_stability,
}
}
fn inject(&self, resolve: &mut Resolve, id: WorldId) -> Result<()> {
// Inject docs/stability for all kebab-named interfaces, both imports
// and exports.
for ((name, data), only_export) in self
.interface_imports_or_exports
.iter()
.map(|p| (p, false))
.chain(self.interface_exports.iter().map(|p| (p, true)))
{
let key = WorldKey::Name(name.to_string());
let world = &mut resolve.worlds[id];
let item = if only_export {
world.exports.get_mut(&key)
} else {
match world.imports.get_mut(&key) {
Some(item) => Some(item),
None => world.exports.get_mut(&key),
}
};
let Some(WorldItem::Interface { id, stability }) = item else {
bail!("missing interface {name:?}");
};
*stability = data.stability.clone();
let id = *id;
data.inject(resolve, id)?;
}
// Process all types, which are always imported, for this world.
for (name, data) in &self.types {
let key = WorldKey::Name(name.to_string());
let Some(WorldItem::Type(id)) = resolve.worlds[id].imports.get(&key) else {
bail!("missing type {name:?}");
};
data.inject(resolve, *id)?;
}
// Build a map of `name_world_key` for interface imports/exports to the
// actual key. This map is then consluted in the next loop.
let world = &resolve.worlds[id];
let stabilities = world
.imports
.iter()
.map(|i| (i, true))
.chain(world.exports.iter().map(|i| (i, false)))
.filter_map(|((key, item), import)| match item {
WorldItem::Interface { .. } => {
Some(((resolve.name_world_key(key), import), key.clone()))
}
_ => None,
})
.collect::<IndexMap<_, _>>();
let world = &mut resolve.worlds[id];
// Update the stability of an interface imports/exports that aren't
// kebab-named.
for ((name, stability), import) in self
.interface_import_stability
.iter()
.map(|p| (p, true))
.chain(self.interface_export_stability.iter().map(|p| (p, false)))
{
let key = match stabilities.get(&(name.clone(), import)) {
Some(key) => key.clone(),
None => bail!("missing interface `{name}`"),
};
let item = if import {
world.imports.get_mut(&key)
} else {
world.exports.get_mut(&key)
};
match item {
Some(WorldItem::Interface { stability: s, .. }) => *s = stability.clone(),
_ => bail!("item `{name}` wasn't an interface"),
}
}
// Update the docs/stability of all functions imported/exported from
// this world.
for ((name, data), only_export) in self
.func_imports_or_exports
.iter()
.map(|p| (p, false))
.chain(self.func_exports.iter().map(|p| (p, true)))
{
let key = WorldKey::Name(name.to_string());
let item = if only_export {
world.exports.get_mut(&key)
} else {
match world.imports.get_mut(&key) {
Some(item) => Some(item),
None => world.exports.get_mut(&key),
}
};
match item {
Some(WorldItem::Function(f)) => data.inject(f)?,
_ => bail!("missing func {name:?}"),
}
}
if let Some(docs) = &self.docs {
world.docs.contents = Some(docs.to_string());
}
world.stability = self.stability.clone();
Ok(())
}
fn is_empty(&self) -> bool {
self.docs.is_none()
&& self.interface_imports_or_exports.is_empty()
&& self.types.is_empty()
&& self.func_imports_or_exports.is_empty()
&& self.stability.is_unknown()
&& self.interface_exports.is_empty()
&& self.func_exports.is_empty()
&& self.interface_import_stability.is_empty()
&& self.interface_export_stability.is_empty()
}
#[cfg(feature = "serde")]
fn is_compatible_with_v0(&self) -> bool {
self.stability.is_unknown()
&& self
.interface_imports_or_exports
.iter()
.all(|(_, w)| w.is_compatible_with_v0())
&& self
.func_imports_or_exports
.iter()
.all(|(_, w)| w.is_compatible_with_v0())
&& self.types.iter().all(|(_, w)| w.is_compatible_with_v0())
// These maps weren't present in v0, so we're only compatible if
// they're empty.
&& self.interface_exports.is_empty()
&& self.func_exports.is_empty()
&& self.interface_import_stability.is_empty()
&& self.interface_export_stability.is_empty()
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
struct InterfaceMetadata {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
docs: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Stability::is_unknown")
)]
stability: Stability,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "StringMap::is_empty")
)]
funcs: StringMap<FunctionMetadata>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "StringMap::is_empty")
)]
types: StringMap<TypeMetadata>,
}
impl InterfaceMetadata {
fn extract(resolve: &Resolve, id: InterfaceId) -> Self {
let interface = &resolve.interfaces[id];
let funcs = interface
.functions
.iter()
.map(|(name, func)| (name.to_string(), FunctionMetadata::extract(func)))
.filter(|(_, item)| !item.is_empty())
.collect();
let types = interface
.types
.iter()
.map(|(name, id)| (name.to_string(), TypeMetadata::extract(resolve, *id)))
.filter(|(_, item)| !item.is_empty())
.collect();
Self {
docs: interface.docs.contents.clone(),
stability: interface.stability.clone(),
funcs,
types,
}
}
fn inject(&self, resolve: &mut Resolve, id: InterfaceId) -> Result<()> {
for (name, data) in &self.types {
let Some(&id) = resolve.interfaces[id].types.get(name) else {
bail!("missing type {name:?}");
};
data.inject(resolve, id)?;
}
let interface = &mut resolve.interfaces[id];
for (name, data) in &self.funcs {
let Some(f) = interface.functions.get_mut(name) else {
bail!("missing func {name:?}");
};
data.inject(f)?;
}
if let Some(docs) = &self.docs {
interface.docs.contents = Some(docs.to_string());
}
interface.stability = self.stability.clone();
Ok(())
}
fn is_empty(&self) -> bool {
self.docs.is_none()
&& self.funcs.is_empty()
&& self.types.is_empty()
&& self.stability.is_unknown()
}
#[cfg(feature = "serde")]
fn is_compatible_with_v0(&self) -> bool {
self.stability.is_unknown()
&& self.funcs.iter().all(|(_, w)| w.is_compatible_with_v0())
&& self.types.iter().all(|(_, w)| w.is_compatible_with_v0())
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged, deny_unknown_fields))]
enum FunctionMetadata {
/// In the v0 format function metadata was only a string so this variant
/// is preserved for the v0 format. In the future this can be removed
/// entirely in favor of just the below struct variant.
///
/// Note that this is an untagged enum so the name `JustDocs` is just for
/// rust.
JustDocs(Option<String>),
/// In the v1+ format we're tracking at least docs but also the stability
/// of functions.
DocsAndStabilty {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
docs: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Stability::is_unknown")
)]
stability: Stability,
},
}
impl FunctionMetadata {
fn extract(func: &Function) -> Self {
if TRY_TO_EMIT_V0_BY_DEFAULT && func.stability.is_unknown() {
FunctionMetadata::JustDocs(func.docs.contents.clone())
} else {
FunctionMetadata::DocsAndStabilty {
docs: func.docs.contents.clone(),
stability: func.stability.clone(),
}
}
}
fn inject(&self, func: &mut Function) -> Result<()> {
match self {
FunctionMetadata::JustDocs(docs) => {
func.docs.contents = docs.clone();
}
FunctionMetadata::DocsAndStabilty { docs, stability } => {
func.docs.contents = docs.clone();
func.stability = stability.clone();
}
}
Ok(())
}
fn is_empty(&self) -> bool {
match self {
FunctionMetadata::JustDocs(docs) => docs.is_none(),
FunctionMetadata::DocsAndStabilty { docs, stability } => {
docs.is_none() && stability.is_unknown()
}
}
}
#[cfg(feature = "serde")]
fn is_compatible_with_v0(&self) -> bool {
match self {
FunctionMetadata::JustDocs(_) => true,
FunctionMetadata::DocsAndStabilty { .. } => false,
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
struct TypeMetadata {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
docs: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Stability::is_unknown")
)]
stability: Stability,
// record fields, variant cases, etc.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "StringMap::is_empty")
)]
items: StringMap<String>,
}
impl TypeMetadata {
fn extract(resolve: &Resolve, id: TypeId) -> Self {
fn extract_items<T>(items: &[T], f: impl Fn(&T) -> (&String, &Docs)) -> StringMap<String> {
items
.iter()
.flat_map(|item| {
let (name, docs) = f(item);
Some((name.to_string(), docs.contents.clone()?))
})
.collect()
}
let ty = &resolve.types[id];
let items = match &ty.kind {
TypeDefKind::Record(record) => {
extract_items(&record.fields, |item| (&item.name, &item.docs))
}
TypeDefKind::Flags(flags) => {
extract_items(&flags.flags, |item| (&item.name, &item.docs))
}
TypeDefKind::Variant(variant) => {
extract_items(&variant.cases, |item| (&item.name, &item.docs))
}
TypeDefKind::Enum(enum_) => {
extract_items(&enum_.cases, |item| (&item.name, &item.docs))
}
// other types don't have inner items
_ => IndexMap::default(),
};
Self {
docs: ty.docs.contents.clone(),
stability: ty.stability.clone(),
items,
}
}
fn inject(&self, resolve: &mut Resolve, id: TypeId) -> Result<()> {
let ty = &mut resolve.types[id];
if !self.items.is_empty() {
match &mut ty.kind {
TypeDefKind::Record(record) => {
self.inject_items(&mut record.fields, |item| (&item.name, &mut item.docs))?
}
TypeDefKind::Flags(flags) => {
self.inject_items(&mut flags.flags, |item| (&item.name, &mut item.docs))?
}
TypeDefKind::Variant(variant) => {
self.inject_items(&mut variant.cases, |item| (&item.name, &mut item.docs))?
}
TypeDefKind::Enum(enum_) => {
self.inject_items(&mut enum_.cases, |item| (&item.name, &mut item.docs))?
}
_ => {
bail!("got 'items' for unexpected type {ty:?}");
}
}
}
if let Some(docs) = &self.docs {
ty.docs.contents = Some(docs.to_string());
}
ty.stability = self.stability.clone();
Ok(())
}
fn inject_items<T: std::fmt::Debug>(
&self,
items: &mut [T],
f: impl Fn(&mut T) -> (&String, &mut Docs),
) -> Result<()> {
let mut unused_docs = self.items.len();
for item in items.iter_mut() {
let (name, item_docs) = f(item);
if let Some(docs) = self.items.get(name.as_str()) {
item_docs.contents = Some(docs.to_string());
unused_docs -= 1;
}
}
if unused_docs > 0 {
bail!(
"not all 'items' match type items; {item_docs:?} vs {items:?}",
item_docs = self.items
);
}
Ok(())
}
fn is_empty(&self) -> bool {
self.docs.is_none() && self.items.is_empty() && self.stability.is_unknown()
}
#[cfg(feature = "serde")]
fn is_compatible_with_v0(&self) -> bool {
self.stability.is_unknown()
}
}