use std::collections::BTreeMap;
use guppy::PackageId;
use rustdoc_processor::crate_data::{
CrateData, CrateItemIndex, CrateItemPaths, EagerCrateItemIndex, EagerCrateItemPaths,
};
use rustdoc_processor::indexing::{CrateIndexer, IndexResult, IndexingVisitor};
use rustdoc_processor::queries::Crate;
use rustdoc_types::Attribute;
#[derive(
Copy,
Clone,
Debug,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
bincode::Encode,
bincode::Decode,
)]
#[expect(
clippy::enum_variant_names,
reason = "the variant names mirror the canonical serde casing literals"
)]
pub enum RenameRule {
PascalCase,
CamelCase,
SnakeCase,
ScreamingSnakeCase,
}
impl RenameRule {
pub fn from_diagnostic_str(s: &str) -> Option<Self> {
match s {
"PascalCase" => Some(Self::PascalCase),
"camelCase" => Some(Self::CamelCase),
"snake_case" => Some(Self::SnakeCase),
"SCREAMING_SNAKE_CASE" => Some(Self::ScreamingSnakeCase),
_ => None,
}
}
pub fn apply(&self, ident: &str) -> String {
use heck::{ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
match self {
Self::PascalCase => ident.to_upper_camel_case(),
Self::CamelCase => ident.to_lower_camel_case(),
Self::SnakeCase => ident.to_snake_case(),
Self::ScreamingSnakeCase => ident.to_shouty_snake_case(),
}
}
}
#[derive(
Clone, Debug, Default, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode,
)]
pub struct ItemAnnotation {
pub export: bool,
pub opaque: bool,
pub skip: bool,
pub rename: Option<String>,
pub prefix_with_name: Option<bool>,
pub field_names: Option<Vec<String>>,
pub rename_all: Option<RenameRule>,
pub rename_all_fields: Option<RenameRule>,
}
#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct CHeadergenAnnotations {
pub items: BTreeMap<rustdoc_types::Id, ItemAnnotation>,
}
impl CHeadergenAnnotations {
pub fn get(&self, id: &rustdoc_types::Id) -> Option<&ItemAnnotation> {
self.items.get(id)
}
}
pub struct CheadergenIndexer;
impl CrateIndexer for CheadergenIndexer {
type Annotations = CHeadergenAnnotations;
fn index_raw(
&self,
krate: rustdoc_types::Crate,
package_id: PackageId,
) -> IndexResult<CHeadergenAnnotations> {
let crate_data = CrateData {
root_item_id: krate.root,
index: CrateItemIndex::Eager(EagerCrateItemIndex { index: krate.index }),
external_crates: krate.external_crates,
format_version: krate.format_version,
paths: CrateItemPaths::Eager(EagerCrateItemPaths { paths: krate.paths }),
};
self.index(crate_data, package_id)
}
fn index(
&self,
crate_data: CrateData,
package_id: PackageId,
) -> IndexResult<CHeadergenAnnotations> {
let mut visitor = CheadergenVisitor::default();
let krate = Crate::index(crate_data, package_id, &mut visitor);
IndexResult {
krate,
annotations: CHeadergenAnnotations {
items: visitor.items,
},
can_cache_indexes: true,
}
}
}
const ATTR_PREFIX: &str = "#[diagnostic::cheadergen::";
fn parse_cheadergen_attr(s: &str, ann: &mut ItemAnnotation) -> bool {
let Some(rest) = s.strip_prefix(ATTR_PREFIX) else {
return false;
};
let rest = rest.strip_suffix(']').unwrap_or(rest);
if rest == "export" {
ann.export = true;
} else if rest == "opaque" {
ann.opaque = true;
} else if rest == "skip" {
ann.skip = true;
} else if let Some(inner) = rest.strip_prefix("rename(") {
let inner = inner.strip_suffix(')').unwrap_or(inner);
let name = inner.trim_matches('"');
ann.rename = Some(name.to_owned());
} else if rest == "prefix_with_name" || rest == "prefix_with_name(true)" {
ann.prefix_with_name = Some(true);
} else if rest == "prefix_with_name(false)" {
ann.prefix_with_name = Some(false);
} else if let Some(inner) = rest.strip_prefix("field_names(") {
let inner = inner.strip_suffix(')').unwrap_or(inner);
let names: Vec<String> = inner.split(',').map(|n| n.trim().to_owned()).collect();
ann.field_names = Some(names);
} else if let Some(inner) = rest.strip_prefix("rename_all(") {
let inner = inner.strip_suffix(')').unwrap_or(inner).trim_matches('"');
ann.rename_all = RenameRule::from_diagnostic_str(inner);
} else if let Some(inner) = rest.strip_prefix("rename_all_fields(") {
let inner = inner.strip_suffix(')').unwrap_or(inner).trim_matches('"');
ann.rename_all_fields = RenameRule::from_diagnostic_str(inner);
} else {
return false;
}
true
}
#[derive(Default)]
struct CheadergenVisitor {
items: BTreeMap<rustdoc_types::Id, ItemAnnotation>,
}
impl IndexingVisitor for CheadergenVisitor {
fn on_item_discovered(&mut self, item: &rustdoc_types::Item, item_id: rustdoc_types::Id) {
let ann = item_annotation_from_attrs(&item.attrs);
if !ann.is_empty() {
self.items.insert(item_id, ann);
}
}
fn on_type_indexed(&mut self, _item_id: rustdoc_types::Id) {}
}
pub fn item_annotation_from_attrs(attrs: &[Attribute]) -> ItemAnnotation {
let mut ann = ItemAnnotation::default();
for attr in attrs {
if let Attribute::Other(s) = attr {
parse_cheadergen_attr(s, &mut ann);
}
}
ann
}
impl ItemAnnotation {
pub fn is_empty(&self) -> bool {
let Self {
export,
opaque,
skip,
rename,
prefix_with_name,
field_names,
rename_all,
rename_all_fields,
} = self;
!export
&& !opaque
&& !skip
&& rename.is_none()
&& prefix_with_name.is_none()
&& field_names.is_none()
&& rename_all.is_none()
&& rename_all_fields.is_none()
}
}
pub struct FieldAnnotation {
pub rename: Option<String>,
pub bitfield_width: Option<u64>,
}
impl FieldAnnotation {
pub fn from_attrs(attrs: &[Attribute]) -> Self {
let mut result = FieldAnnotation {
rename: None,
bitfield_width: None,
};
for attr in attrs {
if let Attribute::Other(s) = attr {
let Some(rest) = s.strip_prefix(ATTR_PREFIX) else {
continue;
};
let rest = rest.strip_suffix(']').unwrap_or(rest);
if let Some(inner) = rest.strip_prefix("rename(") {
let inner = inner.strip_suffix(')').unwrap_or(inner);
let name = inner.trim_matches('"');
result.rename = Some(name.to_owned());
} else if let Some(inner) = rest.strip_prefix("bitfield(") {
let inner = inner.strip_suffix(')').unwrap_or(inner);
let inner = inner.trim().trim_end_matches("u64");
if let Ok(width) = inner.parse::<u64>() {
result.bitfield_width = Some(width);
}
}
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_each_rule() {
assert_eq!(RenameRule::PascalCase.apply("my_status"), "MyStatus");
assert_eq!(RenameRule::CamelCase.apply("MyStatus"), "myStatus");
assert_eq!(RenameRule::SnakeCase.apply("MyStatus"), "my_status");
assert_eq!(
RenameRule::ScreamingSnakeCase.apply("MyStatus"),
"MY_STATUS"
);
}
#[test]
fn from_diagnostic_str_round_trips() {
for (literal, rule) in [
("PascalCase", RenameRule::PascalCase),
("camelCase", RenameRule::CamelCase),
("snake_case", RenameRule::SnakeCase),
("SCREAMING_SNAKE_CASE", RenameRule::ScreamingSnakeCase),
] {
assert_eq!(RenameRule::from_diagnostic_str(literal), Some(rule));
}
assert_eq!(RenameRule::from_diagnostic_str("kebab-case"), None);
}
}